Java类加载机制通过“类名+ClassLoader实例”唯一标识类,实现多版本JAR共存:自定义ClassLoader绕过双亲委派、优先本地加载;SPI接口统一契约、插件fat-jar闭环依赖,确保各版本类隔离不冲突。

Java 类加载机制本身不支持多版本 JAR 包共存,但可以通过类加载器的隔离能力实现逻辑上的共存——关键不是“让 JVM 同时认多个同名类”,而是“让不同版本的类由不同的 ClassLoader 加载,彼此互不可见”。
类唯一性由“类名 + 类加载器”共同决定
JVM 判定两个类是否相同,看的是全限定类名和加载它的 ClassLoader 实例。这意味着:
- v1.jar 中的 com.example.Service 由 PluginClassLoaderV1 加载 → 类型是 PluginClassLoaderV1#com.example.Service
- v2.jar 中的 com.example.Service 由 PluginClassLoaderV2 加载 → 类型是 PluginClassLoaderV2#com.example.Service
- 二者在 JVM 中完全独立,不会相互覆盖,也不会触发 NoClassDefFoundError 或 ClassCastException
必须绕过双亲委派机制
默认的 AppClassLoader 遵循双亲委派,所有请求最终归到系统类加载器,导致只能加载一个版本。要隔离,需自定义 ClassLoader:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 继承 URLClassLoader,重写 loadClass(String name, boolean resolve)
- 在重写方法中,优先调用 findClass(name) 尝试从插件 JAR 加载
- 仅当本地找不到时,才委托给 parent(如需访问 JDK 类)
- 构造时显式传入 null 或最小父加载器,避免意外继承上下文类加载器
接口统一 + 实现隔离
主程序不能直接 new 插件类或强转类型,因为跨加载器的类不可见。必须通过契约解耦:
立即学习“Java免费学习笔记(深入)”;
- 定义 SPI 接口(如 Exporter、Processor),放在主应用 classpath 下,由系统类加载器加载
- 各版本插件 JAR 只含实现类,且不打包该接口(避免重复类)
- 主程序用反射创建实例:Class.forName("v2.impl.ExporterImpl", true, clV2).getDeclaredConstructor().newInstance()
- 持有接口引用调用,参数/返回值限用 JDK 类型(String、Map、byte[])或公共 DTO
依赖与资源必须闭环
插件 JAR 必须自包含全部运行时依赖,否则会出现“传导泄漏”:
- 常用 maven-shade-plugin 打成 fat jar,可选 relocate 重命名内部依赖包(如把 guava 重命名为 shaded.com.google.common)
- 若插件 A 的类调用了 org.apache.commons.lang3.StringUtils,而该类被 AppClassLoader 加载,则后续所有对 StringUtils 的引用都会走主类加载器
- 一旦插件 B 也用到 StringUtils,但期望的是自己 JAR 中的版本,就会行为错乱或抛异常

















