java多版本jar共存的核心是类唯一性判定规则:jvm以“全限定类名+classloader实例”标识类,不同classloader加载的同名类互不兼容;需自定义classloader绕过双亲委派,通过spi接口解耦,并确保插件依赖闭环。

Java 类加载机制实现多版本 Jar 包共存,核心在于利用类的唯一性判定规则:JVM 认为两个类是否相同,取决于“全限定类名 + 加载它的 ClassLoader 实例”二者共同构成的标识。这意味着,只要让不同版本的同名类(如 com.example.Service)由不同的 ClassLoader 实例加载,它们就在 JVM 中互为独立、彼此隔离。
类唯一性是隔离的基础
JVM 不按包名或字节码内容判断类是否重复,而是绑定类与其加载器。例如:
- v1.jar 中的 com.example.Utils 由 PluginClassLoaderV1 加载 → 类型为 PluginClassLoaderV1#com.example.Utils
- v2.jar 中的 com.example.Utils 由 PluginClassLoaderV2 加载 → 类型为 PluginClassLoaderV2#com.example.Utils
二者完全不兼容,不会触发 NoClassDefFoundError 或 ClassCastException,也不会相互覆盖。
自定义 ClassLoader 打破双亲委派
默认的 AppClassLoader 遵循双亲委派,所有请求最终都归到系统类加载器,导致只能加载一个版本。要隔离,必须绕过该流程:
- 继承
URLClassLoader,重写loadClass(String name, boolean resolve) - 在重写方法中,优先调用
findClass(name)尝试从插件 Jar 加载 - 仅当
findClass失败后,才委托给 parent(如需访问 JDK 基础类) - 构造时显式传入
null或指定最小父加载器(避免意外继承上下文类加载器)
接口统一 + 实现隔离
主程序无法直接 new 插件类或强转类型,因为跨加载器的类不可见。必须通过契约解耦:
- 定义 SPI 接口(如
Processor、Exporter)放在主应用 classpath 下,由系统类加载器加载 - 各版本插件 Jar 只含实现类,且不打包该接口(避免重复类)
- 主程序用反射创建实例:
Class.forName("v1.impl.ExporterImpl", true, clV1).getDeclaredConstructor().newInstance() - 持有接口引用调用,参数/返回值限用 JDK 类型(
String、Map、byte[])或公共 DTO
依赖与资源必须闭环
插件 Jar 必须自包含全部运行时依赖(常用 maven-shade-plugin 打成 fat jar),不能依赖主程序 classpath 中的库。否则会出现“传导泄漏”:
- 插件 A 的类调用了
org.apache.commons.lang3.StringUtils - 若该类被 AppClassLoader 加载,则后续所有对 StringUtils 的引用都会走主类加载器
- 一旦插件 B 也用到 StringUtils,但期望的是自己 Jar 里的版本,就会出错
- 因此插件内所有依赖(包括日志、工具类、配置类)都必须由同一自定义 ClassLoader 加载
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











