必须先搞懂双亲委派的“断点”在哪,因为JVM默认委派链中仅loadClass()和findClass()两处可干预;重写loadClass()可跳过父委托,重写findClass()可自定义字节码来源。

直接破坏双亲委派不是目的,而是理解类加载机制、SPI 扩展原理和热部署底层逻辑的必经路径。关键在于:在受控环境下绕过默认委派链,让自定义 ClassLoader 优先加载特定类(如 SPI 实现类或更新后的业务类),从而验证接口契约、替换实现、触发重加载。
为什么必须先搞懂双亲委派的“断点”在哪
JVM 默认的 Bootstrap → Extension → Application 三级委派链,本质是 ClassLoader#loadClass() 方法中先调用 parent.loadClass(),失败后才调用自己的 findClass()。真正可干预的位置只有两个:重写 loadClass() 跳过父委托,或重写 findClass() 提供自己的字节码来源。SPI 的 ServiceLoader 默认走 AppClassLoader,所以若想动态加载新实现,就得让自己的 ClassLoader 成为服务查找的“起点”。
实战 SPI 接口替换:用自定义 ClassLoader 加载新版实现
假设你有一个日志 SPI 接口 LogProvider,原实现 FileLogProvider 在 classpath 下;现在你打包了新版 CloudLogProvider 到一个独立 JAR 文件中,希望不重启就切换:
- 编写一个继承
URLClassLoader的HotSwapClassLoader,构造时传入该 JAR 的 URL,并重写 loadClass(String, boolean) 方法:对 "com.example.spi.LogProvider" 及其实现类,跳过 super.loadClass(),直接调用 findClass() - 调用
ServiceLoader.load(LogProvider.class, hotClassLoader),此时会从 hotClassLoader 的 URL 中扫描META-INF/services/com.example.spi.LogProvider并实例化CloudLogProvider - 注意:原 AppClassLoader 加载的接口类与 hotClassLoader 加载的实现类属于不同运行时包(runtime package),需确保接口由 hotClassLoader 或其父类加载器统一提供(推荐把接口 JAR 也放入 hotClassLoader,或由其 parent 加载)
热部署核心:隔离 + 替换 + 卸载
热部署不是“重新 load 一遍”,而是一次完整的生命周期管理:
- 类隔离:每个版本使用独立 ClassLoader(如按时间戳或版本号命名),避免静态字段污染和类型不兼容
- 主动卸载:显式释放旧 ClassLoader 引用(如清空缓存、关闭资源),并确保无强引用指向其加载的类实例,等待 GC 回收——这是 JVM 卸载类的唯一前提
-
线程上下文切换:通过
Thread.currentThread().setContextClassLoader(newLoader)让框架(如 Spring、JDBC 驱动)感知新环境;SPI 查找默认使用上下文类加载器,这点至关重要
避坑提醒:常见失效原因
很多“热加载失败”其实和代码无关,而是机制被隐式绕过:
- JDBC 驱动注册依赖
ServiceLoader.load(Driver.class),但 DriverManager 内部硬编码使用ClassLoader.getSystemClassLoader(),导致你的 hotClassLoader 加载的驱动无法注册——需反射替换 DriverManager 的 drivers 字段或使用DriverManager.registerDriver(...)显式注册 - Spring Boot 的
@ConditionalOnClass在启动期已解析完成,运行时替换类不会触发条件重判;热部署需配合 Spring Loaded / JRebel 或自研 BeanDefinitionRegistryPostProcessor 动态刷新 - Java 9+ 模块系统下,
ModuleLayer和ModuleFinder会限制类可见性,自定义 ClassLoader 必须实现findModule()并参与模块解析,否则 SPI 查找不到服务

















