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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM