為什么需要類隔離加載
項目開發過程中,需要依賴不同版本的中間件依賴包,以適配不同的中間件服務端
如果這些中間件依賴包版本之間不能向下兼容,高版本依賴無法連接低版本的服務端,相反低版本依賴也無法連接高版本服務端
項目中也不能同時引入兩個版本的中間件依賴,勢必會導致類加載沖突,程序無法正常執行
解決方案
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