jvm通过“全限定名+classloader实例”唯一标识类,实现同名多版本隔离;需自定义classloader打破双亲委派、为每版本创建独立实例、接口由系统加载器统一提供、插件依赖闭环。

JVM 本身不支持同名类的多版本共存,但通过类加载器隔离可以实现逻辑上的“共存”。关键不是让两个版本同时生效,而是让它们互不影响、按需加载。
类唯一性是隔离的前提
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 和 WebappClassLoader 都遵循双亲委派,所有类加载请求最终都会向上委托到启动类加载器,结果只能加载一个版本。
要实现隔离,得自定义 ClassLoader:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 继承 URLClassLoader,重写 loadClass(String name, boolean resolve)
- 在重写方法中,优先调用 findClass(name),从指定 jar 路径加载
- 仅当 findClass 失败时,才委托给 parent(比如需要访问 String、List 等 JDK 类)
- 构造时显式传入 null 或最小父加载器,避免意外继承上下文类加载器
接口契约是跨加载器通信的桥梁
主程序不能直接 new 插件类,也不能强转类型——因为不同 ClassLoader 加载的类不可见。
解耦方式是:
- 把 SPI 接口(如 Exporter、Processor)放在主应用 classpath 下,由系统类加载器加载
- 各插件 jar 只包含实现类,且不打包该接口(否则会引发 LinkageError)
- 主程序用反射创建实例:Class.forName("v2.impl.ExporterImpl", true, clV2).getDeclaredConstructor().newInstance()
- 后续调用全部基于接口引用,参数/返回值限制为 JDK 类型(String、Map、byte[])或共享 DTO
依赖闭环与资源隔离不能妥协
插件 jar 必须是自包含的 fat jar(常用 maven-shade-plugin 打包),不能依赖主程序 classpath 中的库。
否则会出现“传导泄漏”:
- 插件 A 调用了 org.apache.commons.lang3.StringUtils
- 若该类被 AppClassLoader 加载,则后续所有对 StringUtils 的引用都走主类加载器
- 一旦插件 B 也用到 StringUtils,但期望自己 jar 里的版本,就会出错
所以每个插件的运行时依赖必须严格闭环,资源(如配置文件、模板)也需通过自己的 ClassLoader 加载,不依赖线程上下文类加载器。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










