Java自定义ClassLoader实现插件类隔离加载
为什么需要类隔离加载
项目开发过程中,需要依赖不同版本的中间件依赖包,以适配不同的中间件服务端
如果这些中间件依赖包版本之间不能向下兼容,高版本依赖无法连接低版本的服务端,相反低版本依赖也无法连接高版本服务端
项目中也不能同时引入两个版本的中间件依赖,势必会导致类加载冲突,程序无法正常执行
解决方案
1、插件包开发:将不同版本的依赖做成不同的插件包,而不是直接在项目中进行依赖引入,这样不同的依赖版本就是不同的插件包了
2、插件包打包:将插件包打包时合入所有的三方库依赖
3、插件包加载:主程序根据中间件版本加载不同的插件包即可执行业务逻辑即可
插件包开发
此处以commons-lang3依赖举例
新建Maven项目,开发插件包,引入中间件依赖,插件包里面依赖的版本是3.11
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.11</version> </dependency>
获取commons-lang3的StringUtils类全路径,代码如下:
public class PluginProvider { public void test() { // 获取当前的类加载器 System.out.println("Plugin: " + this.getClass().getClassLoader()); // 获取类全路径 System.out.println("Plugin: " + StringUtils.class.getResource("").getPath()); } }
插件包打包
使用maven-assembly-plugin打包插件,将所有依赖包中的class文件打包到Jar包中,pom.xml配置如下:
<plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins>
打包后查看xxx-jar-with-dependencies.jar包结构
主程序加载插件包
主程序依赖commons-lang3的3.12.0版本
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.12.0</version> </dependency>
类加载器的双亲委派机制,先使用父加载器加载class,加载不到时再调用findClass方法
这里我们直接将父加载器设置为NULL,插件包类引用的所有Class重新进行加载,类加载器重构代码如下:
public class PluginClassLoader extends URLClassLoader { public PluginClassLoader(URL[] urls) { // 类加载器的双亲委派机制 // 先使用父加载器加载class,加载不到时再调用findClass方法 super(urls, null); } }
将插件包放在/resources/plugin/目录中,如图所示:
调用插件包代码如下:
public class PluginTester { @PostConstruct public void test() { // 打印当前类加载器 System.out.println("Boot: " + this.getClass().getClassLoader()); // 获取StringUtils的类全路径 System.out.println("Boot: " + StringUtils.class.getResource("").getPath()); // 模拟调用插件包 testPlugin(); } public void testPlugin() { try { // 加载插件包 ClassPathResource resource = new ClassPathResource("plugin/plugin-provider.jar"); // 打印插件包路径 System.out.println(resource.getURL().getPath()); // URLClassLoader classLoader = new URLClassLoader(new URL[]{resource.getURL()}); // 初始化自己的ClassLoader PluginClassLoader pluginClassLoader = new PluginClassLoader(new URL[]{resource.getURL()}); // 这里需要临时更改当前线程的 ContextClassLoader // 避免中间件代码中存在Thread.currentThread().getContextClassLoader()获取类加载器 // 因为它们会获取当前线程的 ClassLoader 来加载 class,而当前线程的ClassLoader极可能是App ClassLoader而非自定义的ClassLoader, 也许是为了安全起见,但是这会导致它可能加载到启动项目中的class(如果有),或者发生其它的异常,所以我们在执行时需要临时的将当前线程的ClassLoader设置为自定义的ClassLoader,以实现绝对的隔离执行 ClassLoader originClassLoader = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(pluginClassLoader); // 加载插件包中的类 Class<?> clazz = pluginClassLoader.loadClass("cn.codest.PluginProvider"); // 反射执行 clazz.getDeclaredMethod("test", null).invoke(clazz.newInstance(), null); Thread.currentThread().setContextClassLoader(originClassLoader); } catch (Exception e) { e.printStackTrace(); } } }
执行结果如下:
// 打印主程序的类加载器
Boot: sun.misc.Launcher$AppClassLoader@18b4aac2
// 打印主程序中依赖的StringUtils全路径 Boot: file:/D:/Codest/Maven_aliyun/repository/org/apache/commons/commons-lang3/3.12.0/commons-lang3-3.12.0.jar!/org/apache/commons/lang3/
// 打印插件包路径 /D:/Codest/Idea/projects/tester/plugin-boot/target/classes/plugin/plugin-provider.jar
// 打印插件包中的类加载器 Plugin: cn.codest.pluginboot.PluginClassLoader@45a4b042
// 打印插件包中的StringUtils全路径 Plugin: file:/D:/Codest/Idea/projects/tester/plugin-boot/target/classes/plugin/plugin-provider.jar!/org/apache/commons/lang3/
通过打印信息可以看出,主程序和插件包中加载的StringUtils分别来自3.12.0的Jar包和插件包中打包的3.11版本。
源码仓库:https://github.com/23557544/blog/tree/master/plugin-class-loader
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)