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

posted @ 2022-01-26 13:40  codest  阅读(445)  评论(0编辑  收藏  举报