可以做到,但需打破双亲委派机制:通过自定义urlclassloader重写loadclass方法跳过父委派,为不同版本jar创建独立加载器实例,利用“全限定名+加载器实例”唯一性实现隔离,适用于插件化、多租户等场景。

可以做到,但需要主动打破双亲委派机制,并自行管理类加载逻辑。JVM 本身不禁止同一类被不同类加载器加载,只要它们属于不同的类加载器实例,就视为完全独立的类——哪怕全限定名一模一样,JVM 也认为是两个不同的类。
为什么默认情况下无法共存
Java 默认采用双亲委派模型:每个类加载器在加载类前,先委托父加载器尝试加载;只有父加载器无法加载时,才由自己查找并定义类。这意味着,如果 commons-lang3-3.4.jar 和 commons-lang3-3.9.jar 都放在 classpath 下,system 类加载器只会加载它最先扫描到的那个版本(顺序依赖 jar 包文件排列或 Class-Path 清单),另一个版本根本不会被使用。
实现隔离的关键:自定义类加载器 + 手动控制委派
要让两个版本同时生效,必须绕过默认委派流程:
- 编写一个继承 URLClassLoader 的子类,重写 loadClass(String name, boolean resolve) 方法
- 在重写方法中,**跳过对父加载器的调用**(即不调用
super.loadClass()),而是直接调用 findClass(name) 从指定路径(如某个独立的 lib 目录)加载类 - 确保两个版本的 Jar 包分别放在互不重叠的路径下,例如:
/plugins/v3.4/和/plugins/v3.9/ - 为每个版本创建独立的类加载器实例,且彼此无父子关系(避免交叉加载)
实际使用时的注意事项
类隔离不是“开箱即用”的功能,而是一种有代价的权衡:
-
对象无法直接传递:由 loader-A 加载的
org.apache.commons.lang3.StringUtils和由 loader-B 加载的同名类,在 JVM 中是完全不同的类型,不能强转、不能作为同一接口的实现混用,传参会抛ClassCastException - 需统一入口契约:推荐通过标准接口(如自定义 SPI 接口或 Java 标准 ServiceLoader 协议)解耦。让两个版本的 Jar 都实现同一个你定义的 interface,业务代码只面向该 interface 编程
- 资源与静态状态不共享:每个类加载器加载的类拥有独立的静态变量空间,日志配置、单例、缓存等都各自维护,需额外协调
-
线程上下文类加载器(TCCL)要显式设置:若调用链涉及框架(如 Spring、Dubbo),它们常依赖 TCCL 查找资源或类,需在执行前用
Thread.currentThread().setContextClassLoader(myLoader)切换
典型适用场景
这种方案常见于插件化系统、多租户 SaaS 后端、中间件 SDK 隔离等对版本强隔离有硬性要求的架构中:
- 一个 Web 应用中,A 插件依赖 fastjson 1.2.76,B 插件依赖 fastjson 2.0.42,二者不能互相干扰
- 微服务网关需同时兼容多个下游系统的旧版/新版通信协议 SDK,各 SDK 内部依赖的 httpclient 版本冲突
- 测试框架动态加载不同版本的待测库进行兼容性验证










