关键在于用独立classloader隔离不同spring版本:每个版本使用专属urlclassloader,重写loadclass优先本地加载、切断双亲委派,并通过共享接口解耦实现,同时重写getresource及管控线程上下文类加载器。

要让同一个服务里同时跑多个版本的 Spring(比如 Spring 5.3 和 Spring 6.1),关键不是“升级”或“替换”,而是让不同版本的 Spring 类彼此不可见、互不干扰。JVM 默认不允许同名类(如 org.springframework.context.ApplicationContext)被不同加载器重复定义,除非你主动切断双亲委派——这正是自定义类加载器的核心任务。
自定义 ClassLoader 必须绕过默认委派链
JVM 的双亲委派机制会把所有类加载请求层层上抛,最终由 AppClassLoader 统一处理。一旦它加载了某个 Spring 类(比如 spring-context-5.3.jar 中的 ClassPathXmlApplicationContext),后续任何加载器再尝试加载同名类都会失败或被忽略。
所以必须让每个 Spring 版本走自己的加载路径:
- 继承
URLClassLoader(而非裸ClassLoader,它已内置资源定位逻辑) - 重写
loadClass(String name, boolean resolve)方法 - 在方法开头先查缓存:
findLoadedClass(name) - 若未加载过,优先调用
findClass(name)从指定路径(如/plugins/spring-6.1/)加载字节码 - 仅当
findClass抛出ClassNotFoundException时,才调用super.loadClass(...)委托给父加载器(用于加载java.*、javax.*等基础类)
这样,spring-core-6.1.jar 里的 BeanFactory 和 spring-core-5.3.jar 里的同名类,在 JVM 看来就是两个完全无关的类型。
每个 Spring 版本必须独占一个 ClassLoader 实例
不能只写一个 SpringVersionClassLoader 类就完事。必须为每个版本创建独立实例:
ClassLoader spring5Loader = new PluginClassLoader("/plugins/spring-5.3/");
ClassLoader spring6Loader = new PluginClassLoader("/plugins/spring-6.1/");
只有这样,spring5Loader.loadClass("org.springframework.beans.factory.BeanFactory") 和 spring6Loader.loadClass("org.springframework.beans.factory.BeanFactory") 才会返回两个不同的 Class> 对象。它们的静态字段、类型转换、instanceof 判断全部隔离——这是多版本共存的底层前提。
接口与实现必须分离,避免跨加载器引用
Spring 自身大量使用反射、泛型、Class.forName,如果业务代码直接 new ClassPathXmlApplicationContext(),就会触发当前线程上下文类加载器(通常是 AppClassLoader)去加载,破坏隔离。
正确做法是:
Java JDK 25 来自 OpenJDK 官方归档,版本为 JDK 25,本条下载地址已指向官方 Windows x64 zip 安装包直链,适合调试旧项目或兼容旧版 Java 运行环境。
- 定义一套轻量级公共接口(如
SpringContainer),由系统类加载器(AppClassLoader)加载,确保所有插件都能看见 - 各版本 Spring 的具体实现(如
Spring5ContainerImpl、Spring6ContainerImpl)打包进各自 jar,并只通过对应 ClassLoader 加载 - 插件启动时,用各自的 ClassLoader 加载实现类,然后向上转型为公共接口,供主程序调用
这样主程序只依赖接口,不碰具体 Spring 类,也就不会意外触发跨加载器类引用。
资源加载和线程上下文需同步适配
Spring 初始化过程会读取 META-INF/spring.factories、扫描 @Component、解析 XML 配置等,这些都依赖 ClassLoader.getResource()。若没重写该方法,可能拿到父加载器的资源,导致行为错乱。
因此:
- 重写
getResource(String name)和getResources(String name),确保只查找当前插件目录下的资源 - 在插件启动前,临时设置线程上下文类加载器:
Thread.currentThread().setContextClassLoader(spring6Loader);
- 初始化完成后及时还原,避免污染其他模块
否则 ServiceLoader.load(ApplicationContextInitializer.class) 可能加载到错误版本的实现,或 ResourcePatternResolver 找不到 classpath*:META-INF/*.xml。
不建议只重写 findClass(),那是常见误区
很多教程说“只要重写 findClass 就行”,但这是误导。findClass 只负责字节码加载,而 loadClass 才控制整个委派流程。如果你不重写 loadClass,默认逻辑仍会先委托父加载器——等于白搭。
真正起效的方式是:
- 显式在
loadClass中拦截特定包名(如"org.springframework.")并强制本地加载 - 或统一策略:所有类优先本地找,找不到再委派(适合插件场景)
- 构造加载器时传入
null作为 parent,彻底切断继承链(慎用,需自行保障基础类可用)
JDK 9+ 还需注意模块封装限制:defineClass 对非 java.* 包校验更严,建议用 URLClassLoader + addURL() 方式加载 jar,比手动 defineClass(byte[]) 更稳妥。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










