实现多版本类并存与热加载的关键是:不同ClassLoader加载同名类形成隔离,类唯一性由“全限定名+ClassLoader”决定;必须通过稳定接口抽象、子优先类加载策略、资源隔离及清理残留引用等配套机制协同实现。

要实现不同版本变量(实际指类或依赖库)在运行时并存,并支持热加载,关键不是让“变量”本身多版本共存,而是让**同名类的不同版本由不同 ClassLoader 加载,形成逻辑隔离**。JVM 不允许同一 ClassLoader 重复定义同名类,但允许不同 ClassLoader 各自加载自己的版本——前提是调用链不跨加载器混用。
核心前提:类唯一性由“全限定名 + ClassLoader 实例”共同决定
Java 规范明确:即使两个类字节码完全相同、包名类名一致,只要加载它们的 ClassLoader 实例不同,JVM 就视其为两个完全独立、互不兼容的类型。这意味着:
- com.example.Service v1.0 由 CL-A 加载 → 类型是 com.example.Service@CL-A
- com.example.Service v2.0 由 CL-B 加载 → 类型是 com.example.Service@CL-B
- 二者不能直接赋值、转型或作为同一方法参数传递,否则抛
ClassCastException
必须配套接口抽象与契约隔离
仅靠多个 ClassLoader 加载不同版本的类还不够——业务代码若直接 new 或强依赖具体类,就会卡死在类型不兼容上。真正可行的路径是:
- 定义稳定不变的公共接口(如
Processor、RuleEngine),放在共享类路径(如启动类加载器能访问的位置) - 各版本实现类(
ProcessorImplV1、ProcessorImplV2)打包进各自模块 JAR,由独立 ClassLoader 加载 - 主程序只面向接口编程,通过反射或工厂创建实例,不引用任何实现类的全限定名
这样,v1 和 v2 的内部可自由使用各自版本的 Guava、Jackson 等依赖,彼此无感知。
热加载需切断双亲委派并清理残留引用
默认 loadClass 走双亲委派,容易把业务类误交由 AppClassLoader 加载,导致版本被覆盖。正确做法是重写策略,对业务包走“子优先”:
- 重写
loadClass(String name, boolean resolve):对com.yourcompany.*等前缀,直接调用findClass(name),跳过父委托 - 对
java.*、javax.*、org.slf4j.*等基础类,仍委托父加载器,避免破坏 JVM 运行环境 - 热卸载前必须清理:调用
DriverManager.deregisterDriver()、LogManager.reset()、清空静态缓存、重置线程上下文类加载器(Thread.currentThread().setContextClassLoader(oldCL))
资源与配置也要跟随 ClassLoader 隔离
Spring Boot 的 application.yml、MyBatis 的 mapper.xml 等资源,默认通过当前线程上下文类加载器查找。若没重写 getResources(),就会漏掉模块内资源:
- 重写
getResources(String name):先用findResources(name)查本模块路径,再合并父加载器结果,确保本模块资源优先 - 配置文件路径尽量用绝对
file:URL(如spring.config.location=file:/path/to/module/app.yml),绕过 classpath 查找逻辑 - 扫描
classpath*:mapper/**/*.xml时,确认ResourcePatternResolver底层使用的 ClassLoader 是当前模块实例


















