为什么需要类隔离加载

项目开发过程中,需要依赖不同版本的中间件依赖包,以适配不同的中间件服务端

如果这些中间件依赖包版本之间不能向下兼容,高版本依赖无法连接低版本的服务端,相反低版本依赖也无法连接高版本服务端

项目中也不能同时引入两个版本的中间件依赖,势必会导致类加载冲突,程序无法正常执行

解决方案

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

Java自定义ClassLoader实现插件类隔离加载的更多相关文章

  1. Java虚拟机JVM学习02 类的加载概述

    Java虚拟机JVM学习02 类的加载概述 类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对 ...

  2. java 27 - 1 反射之 类的加载器

    说到反射,首先说类的加载器. 类的加载: 当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载,连接,初始化三步来实现对这个类进行初始化. 加载: 就是指将class文件读入内存,并为之 ...

  3. Java温故而知新(10)类的加载机制

    类加载是Java程序运行的第一步,研究类的加载有助于了解JVM执行过程,并指导开发者采取更有效的措施配合程序执行. 研究类加载机制的第二个目的是让程序能动态的控制类加载,比如热部署等,提高程序的灵活性 ...

  4. 透过现象看本质:Java类动态加载和热替换

    摘要:本文主要介绍类加载器.自定义类加载器及类的加载和卸载等内容,并举例介绍了Java类的热替换. 最近,遇到了两个和Java类的加载和卸载相关的问题: 1) 是一道关于Java的判断题:一个类被首次 ...

  5. java类的加载机制

    什么是类装载器ClassLoader ClassLoader是一个抽象类 ClassLoader的实例将读入Java字节码将类装载到JVM中 ClassLoader可以定制,满足不同的字节码流获取方式 ...

  6. java类的加载、链接、初始化

    JVM和类的关系 当我们调用JAVA命令运行某个java程序时,该命令将会启动一条java虚拟机进程,不管该java程序有多么复杂,该程序启动了多少个线程,它们都处于该java虚拟机进程里.正如前面介 ...

  7. java 类的加载、连接和初始化

    JVM和类 调用Java命令运行Java程序时,该命令将会启动一条Java虚拟机进程,不管该Java程序启动了多少条线程,创建了多少个变量,它们都处于该Java虚拟机进程里,共享该JVM进程的内存区. ...

  8. <JVM中篇:字节码与类的加载篇>04-再谈类的加载器

    笔记来源:尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机) 同步更新:https://gitee.com/vectorx/NOTE_JVM https://codechina.cs ...

  9. 别翻了,这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析【JVM篇二】

    目录 1.什么是类的加载(类初始化) 2.类的生命周期 3.接口的加载过程 4.解开开篇的面试题 5.理解首次主动使用 6.类加载器 7.关于命名空间 8.JVM类加载机制 9.双亲委派模型 10.C ...

  10. java 反射,类的加载过程以及Classloader类加载器

    首先自定义一个类Person package reflection; public class Person { private String name; public int age; public ...

随机推荐

  1. redis集群:MASTER aborted replication with an error: NOAUTH Authentication required.

    发现个问题:redis集群所在服务器,磁盘空间很快就被占满,使用 "du -sh *"查看每个文件夹的大小,发现redis集群三个从节点的日志文件占用空间很大. 下面记录问题排查及 ...

  2. 后缀数组C++详解

    后缀定义 "后缀i"代表以第i个字符开头的后缀,存储是用i代表字符串s的后缀s[i...n] 后缀数组是什么? 后缀数组(Suffix Array)主要关系到两个数组:sa 和 r ...

  3. 知识图谱(Knowledge Graph)- Neo4j 5.10.0 Desktop & GraphXR 连接自建数据库

    #输入查看数据库连接 neo4j$ :server status 添加 远程连接,输入连接地址 Graph Apps 选择 GraphXR 打开 显示

  4. 2023-08-24:请用go语言编写。给定一个长度为n的数组arr, 现在你有一次机会, 将其中连续的K个数全修改成任意一个值, 请你计算如何修改可以使修改后的数 列的最长不下降子序列最长。 请输出

    2023-08-24:请用go语言编写.给定一个长度为n的数组arr, 现在你有一次机会, 将其中连续的K个数全修改成任意一个值, 请你计算如何修改可以使修改后的数 列的最长不下降子序列最长. 请输出 ...

  5. 《深入理解Java虚拟机》读书笔记:方法调用

      方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程.在程序运行时,进行方法调用是最普遍.最频繁的操作,但前面已经讲过 ...

  6. python 自定义排序

    需求:根据自定义的顺序就行排序 实现方法: res = [ {'name': 'RE', 'value': 2}, {'name': 'aa', 'value': 3}, {'name': 'RFM' ...

  7. 《Python魔法大冒险》009 魔法之语:字符串的奥秘

    随着小鱼和魔法师的深入,他们来到了一个被薄雾笼罩的湖泊.湖中央有一个小岛,岛上有一棵巨大的古树,树上挂满了闪闪发光的果实,每一个果实上都刻着一个字母或符号. 小鱼好奇地问:"这些是什么果实? ...

  8. 为不断增长的Go生态系统扩展gopls

    原文在这里. 由 Robert Findley and Alan Donovan 发布于 2023年9月8日 今年夏天初,Go团队发布了gopls的v0.12版本,这是Go语言的语言服务器,它进行了核 ...

  9. MySQL中的Statistics等待

    [作者] 吴宙旭,携程数据库专家 [问题描述] 线上我们偶尔会碰到MySQL的状态是statistics. 但如果出现大量的statistics等待,会引起MySQL性能急剧下降.官方的文档对这个状态 ...

  10. js递归查询之根据id查询当前对象

    需求:递归查询数组中id对应的数据 Json数据格式: 1 let cityArr = [ 2 { 3 id: 1000, 4 name: '四川省', 5 children: [ 6 { 7 id: ...