如何实现Java类隔离加载?
一 什么是類(lèi)隔離技術(shù)
只要你 Java 代碼寫(xiě)的足夠多,就一定會(huì)出現(xiàn)這種情況:系統(tǒng)新引入了一個(gè)中間件的 jar 包,編譯的時(shí)候一切正常,一運(yùn)行就報(bào)錯(cuò):java.lang.NoSuchMethodError,然后就哼哧哼哧的開(kāi)始找解決方法,最后在幾百個(gè)依賴包里面找的眼睛都快瞎了才找到?jīng)_突的 jar,把問(wèn)題解決之后就開(kāi)始吐槽中間件為啥搞那么多不同版本的 jar,寫(xiě)代碼五分鐘,排包排了一整天。
上面這種情況就是 Java 開(kāi)發(fā)過(guò)程中常見(jiàn)的情況,原因也很簡(jiǎn)單,不同 jar 包依賴了某些通用 jar 包(如日志組件)的版本不一樣,編譯的時(shí)候沒(méi)問(wèn)題,到了運(yùn)行時(shí)就會(huì)因?yàn)榧虞d的類(lèi)跟預(yù)期不符合導(dǎo)致報(bào)錯(cuò)。舉個(gè)例子:A 和 B 分別依賴了 C 的 v1 和 v2 版本,v2 版本的 Log 類(lèi)比 v1 版本新增了 error 方法,現(xiàn)在工程里面同時(shí)引入了 A、B 兩個(gè) jar 包,以及 C 的 v0.1、v0.2 版本,打包的時(shí)候 maven 只能選擇一個(gè) C 的版本,假設(shè)選擇了 v1 版本。到了運(yùn)行的時(shí)候,默認(rèn)情況下一個(gè)項(xiàng)目的所有類(lèi)都是用同一個(gè)類(lèi)加載器加載的,所以不管你依賴了多少個(gè)版本的 C,最終只會(huì)有一個(gè)版本的 C 被加載到 JVM 中。當(dāng) B 要去訪問(wèn) Log.error,就會(huì)發(fā)現(xiàn) Log 壓根就沒(méi)有 error 方法,然后就拋異常java.lang.NoSuchMethodError。這就是類(lèi)沖突的一個(gè)典型案例。
類(lèi)沖突的問(wèn)題如果版本是向下兼容的其實(shí)很好解決,把低版本的排除掉就完事了。但要是遇到版本不向下兼容的那就陷入了“救媽媽還是救女朋友”的兩難處境了。
為了避免兩難選擇,有人就提出了類(lèi)隔離技術(shù)來(lái)解決類(lèi)沖突的問(wèn)題。類(lèi)隔離的原理也很簡(jiǎn)單,就是讓每個(gè)模塊使用獨(dú)立的類(lèi)加載器來(lái)加載,這樣不同模塊之間的依賴就不會(huì)互相影響。如下圖所示,不同的模塊用不同的類(lèi)加載器加載。為什么這樣做就能解決類(lèi)沖突呢?這里用到了 Java 的一個(gè)機(jī)制:不同類(lèi)加載器加載的類(lèi)在 JVM 看來(lái)是兩個(gè)不同的類(lèi),因?yàn)樵?JVM 中一個(gè)類(lèi)的唯一標(biāo)識(shí)是 類(lèi)加載器+類(lèi)名。通過(guò)這種方式我們就能夠同時(shí)加載 C 的兩個(gè)不同版本的類(lèi),即使它類(lèi)名是一樣的。注意,這里類(lèi)加載器指的是類(lèi)加載器的實(shí)例,并不是一定要定義兩個(gè)不同類(lèi)加載器,例如圖中的 PluginClassLoaderA 和 PluginClassLoaderB 可以是同一個(gè)類(lèi)加載器的不同實(shí)例。
二 如何實(shí)現(xiàn)類(lèi)隔離
前面我們提到類(lèi)隔離就是讓不同模塊的 jar 包用不同的類(lèi)加載器加載,要做到這一點(diǎn),就需要讓 JVM 能夠使用自定義的類(lèi)加載器加載我們寫(xiě)的類(lèi)以及其關(guān)聯(lián)的類(lèi)。
那么如何實(shí)現(xiàn)呢?一個(gè)很簡(jiǎn)單的做法就是 JVM 提供一個(gè)全局類(lèi)加載器的設(shè)置接口,這樣我們直接替換全局類(lèi)加載器就行了,但是這樣無(wú)法解決多個(gè)自定義類(lèi)加載器同時(shí)存在的問(wèn)題。
實(shí)際上 JVM 提供了一種非常簡(jiǎn)單有效的方式,我把它稱(chēng)為類(lèi)加載傳導(dǎo)規(guī)則:JVM 會(huì)選擇當(dāng)前類(lèi)的類(lèi)加載器來(lái)加載所有該類(lèi)的引用的類(lèi)。例如我們定義了 TestA 和 TestB 兩個(gè)類(lèi),TestA 會(huì)引用 TestB,只要我們使用自定義的類(lèi)加載器加載 TestA,那么在運(yùn)行時(shí),當(dāng) TestA 調(diào)用到 TestB 的時(shí)候,TestB 也會(huì)被 JVM 使用 TestA 的類(lèi)加載器加載。依此類(lèi)推,只要是 TestA 及其引用類(lèi)關(guān)聯(lián)的所有 jar 包的類(lèi)都會(huì)被自定義類(lèi)加載器加載。通過(guò)這種方式,我們只要讓模塊的 main 方法類(lèi)使用不同的類(lèi)加載器加載,那么每個(gè)模塊的都會(huì)使用 main 方法類(lèi)的類(lèi)加載器加載的,這樣就能讓多個(gè)模塊分別使用不同類(lèi)加載器。這也是 OSGi 和 SofaArk 能夠?qū)崿F(xiàn)類(lèi)隔離的核心原理。
了解了類(lèi)隔離的實(shí)現(xiàn)原理之后,我們從重寫(xiě)類(lèi)加載器開(kāi)始進(jìn)行實(shí)操。要實(shí)現(xiàn)自己的類(lèi)加載器,首先讓自定義的類(lèi)加載器繼承 java.lang.ClassLoader,然后重寫(xiě)類(lèi)加載的方法,這里我們有兩個(gè)選擇,一個(gè)是重寫(xiě) findClass(String name),一個(gè)是重寫(xiě) loadClass(String name)。那么到底應(yīng)該選擇哪個(gè)?這兩者有什么區(qū)別?
下面我們分別嘗試重寫(xiě)這兩個(gè)方法來(lái)實(shí)現(xiàn)自定義類(lèi)加載器。
1 重寫(xiě) findClass
首先我們定義兩個(gè)類(lèi),TestA 會(huì)打印自己的類(lèi)加載器,然后調(diào)用 TestB 打印它的類(lèi)加載器,我們預(yù)期是實(shí)現(xiàn)重寫(xiě)了 findClass 方法的類(lèi)加載器 MyClassLoaderParentFirst 能夠在加載了 TestA 之后,讓 TestB 也自動(dòng)由 MyClassLoaderParentFirst 來(lái)進(jìn)行加載。
public class TestA {public static void main(String[] args) {TestA testA = new TestA();testA.hello();}public void hello() {System.out.println("TestA: " + this.getClass().getClassLoader());TestB testB = new TestB();testB.hello();} }public class TestB {public void hello() {System.out.println("TestB: " + this.getClass().getClassLoader());} }然后重寫(xiě)一下 findClass 方法,這個(gè)方法先根據(jù)文件路徑加載 class 文件,然后調(diào)用 defineClass 獲取 Class 對(duì)象。
public class MyClassLoaderParentFirst extends ClassLoader{private Map<String, String> classPathMap = new HashMap<>();public MyClassLoaderParentFirst() {classPathMap.put("com.java.loader.TestA", "/Users/hansong/IdeaProjects/OhMyJava/CodeRepository/target/classes/com/java/loader/TestA.class");classPathMap.put("com.java.loader.TestB", "/Users/hansong/IdeaProjects/OhMyJava/CodeRepository/target/classes/com/java/loader/TestB.class");}// 重寫(xiě)了 findClass 方法@Overridepublic Class<?> findClass(String name) throws ClassNotFoundException {String classPath = classPathMap.get(name);File file = new File(classPath);if (!file.exists()) {throw new ClassNotFoundException();}byte[] classBytes = getClassData(file);if (classBytes == null || classBytes.length == 0) {throw new ClassNotFoundException();}return defineClass(classBytes, 0, classBytes.length);}private byte[] getClassData(File file) {try (InputStream ins = new FileInputStream(file); ByteArrayOutputStream baos = newByteArrayOutputStream()) {byte[] buffer = new byte[4096];int bytesNumRead = 0;while ((bytesNumRead = ins.read(buffer)) != -1) {baos.write(buffer, 0, bytesNumRead);}return baos.toByteArray();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}return new byte[] {};} }最后寫(xiě)一個(gè) main 方法調(diào)用自定義的類(lèi)加載器加載 TestA,然后通過(guò)反射調(diào)用 TestA 的 main 方法打印類(lèi)加載器的信息。
public class MyTest {public static void main(String[] args) throws Exception {MyClassLoaderParentFirst myClassLoaderParentFirst = new MyClassLoaderParentFirst();Class testAClass = myClassLoaderParentFirst.findClass("com.java.loader.TestA");Method mainMethod = testAClass.getDeclaredMethod("main", String[].class);mainMethod.invoke(null, new Object[]{args});}執(zhí)行的結(jié)果如下:
TestA: com.java.loader.MyClassLoaderParentFirst@1d44bcfa TestB: sun.misc.Launcher$AppClassLoader@18b4aac2執(zhí)行的結(jié)果并沒(méi)有如我們期待,TestA 確實(shí)是 MyClassLoaderParentFirst 加載的,但是 TestB 還是 AppClassLoader 加載的。這是為什么呢?
要回答這個(gè)問(wèn)題,首先是要了解一個(gè)類(lèi)加載的規(guī)則:JVM 在觸發(fā)類(lèi)加載時(shí)調(diào)用的是 ClassLoader.loadClass 方法。這個(gè)方法的實(shí)現(xiàn)了雙親委派:
- 委托給父加載器查詢
- 如果父加載器查詢不到,就調(diào)用 findClass 方法進(jìn)行加載
明白了這個(gè)規(guī)則之后,執(zhí)行的結(jié)果的原因就找到了:JVM 確實(shí)使用了MyClassLoaderParentFirst 來(lái)加載 TestB,但是因?yàn)殡p親委派的機(jī)制,TestB 被委托給了 MyClassLoaderParentFirst 的父加載器 AppClassLoader 進(jìn)行加載。
你可能還好奇,為什么 MyClassLoaderParentFirst 的父加載器是 AppClassLoader?因?yàn)槲覀兌x的 main 方法類(lèi)默認(rèn)情況下都是由 JDK 自帶的 AppClassLoader 加載的,根據(jù)類(lèi)加載傳導(dǎo)規(guī)則,main 類(lèi)引用的 MyClassLoaderParentFirst 也是由加載了 main 類(lèi)的AppClassLoader 來(lái)加載。由于 MyClassLoaderParentFirst 的父類(lèi)是 ClassLoader,ClassLoader 的默認(rèn)構(gòu)造方法會(huì)自動(dòng)設(shè)置父加載器的值為 AppClassLoader。
protected ClassLoader() {this(checkCreateClassLoader(), getSystemClassLoader()); }2 重寫(xiě) loadClass
由于重寫(xiě) findClass 方法會(huì)受到雙親委派機(jī)制的影響導(dǎo)致 TestB 被 AppClassLoader 加載,不符合類(lèi)隔離的目標(biāo),所以我們只能重寫(xiě) loadClass 方法來(lái)破壞雙親委派機(jī)制。代碼如下所示:
public class MyClassLoaderCustom extends ClassLoader {private ClassLoader jdkClassLoader;private Map<String, String> classPathMap = new HashMap<>();public MyClassLoaderCustom(ClassLoader jdkClassLoader) {this.jdkClassLoader = jdkClassLoader;classPathMap.put("com.java.loader.TestA", "/Users/hansong/IdeaProjects/OhMyJava/CodeRepository/target/classes/com/java/loader/TestA.class");classPathMap.put("com.java.loader.TestB", "/Users/hansong/IdeaProjects/OhMyJava/CodeRepository/target/classes/com/java/loader/TestB.class");}@Overrideprotected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {Class result = null;try {//這里要使用 JDK 的類(lèi)加載器加載 java.lang 包里面的類(lèi)result = jdkClassLoader.loadClass(name);} catch (Exception e) {//忽略}if (result != null) {return result;}String classPath = classPathMap.get(name);File file = new File(classPath);if (!file.exists()) {throw new ClassNotFoundException();}byte[] classBytes = getClassData(file);if (classBytes == null || classBytes.length == 0) {throw new ClassNotFoundException();}return defineClass(classBytes, 0, classBytes.length);}private byte[] getClassData(File file) { //省略 }}這里注意一點(diǎn),我們重寫(xiě)了 loadClass 方法也就是意味著所有類(lèi)包括 java.lang 包里面的類(lèi)都會(huì)通過(guò) MyClassLoaderCustom 進(jìn)行加載,但類(lèi)隔離的目標(biāo)不包括這部分 JDK 自帶的類(lèi),所以我們用 ExtClassLoader 來(lái)加載 JDK 的類(lèi),相關(guān)的代碼就是:result = jdkClassLoader.loadClass(name);
測(cè)試代碼如下:
public class MyTest {public static void main(String[] args) throws Exception {//這里取AppClassLoader的父加載器也就是ExtClassLoader作為MyClassLoaderCustom的jdkClassLoaderMyClassLoaderCustom myClassLoaderCustom = new MyClassLoaderCustom(Thread.currentThread().getContextClassLoader().getParent());Class testAClass = myClassLoaderCustom.loadClass("com.java.loader.TestA");Method mainMethod = testAClass.getDeclaredMethod("main", String[].class);mainMethod.invoke(null, new Object[]{args});} }執(zhí)行結(jié)果如下:
TestA: com.java.loader.MyClassLoaderCustom@1d44bcfa TestB: com.java.loader.MyClassLoaderCustom@1d44bcfa可以看到,通過(guò)重寫(xiě)了 loadClass 方法,我們成功的讓 TestB 也使用MyClassLoaderCustom 加載到了 JVM 中。
三 總結(jié)
類(lèi)隔離技術(shù)是為了解決依賴沖突而誕生的,它通過(guò)自定義類(lèi)加載器破壞雙親委派機(jī)制,然后利用類(lèi)加載傳導(dǎo)規(guī)則實(shí)現(xiàn)了不同模塊的類(lèi)隔離。
參考資料
深入探討 Java 類(lèi)加載器
(https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html)
原文鏈接:https://developer.aliyun.com/article/780697?
版權(quán)聲明:本文內(nèi)容由阿里云實(shí)名注冊(cè)用戶自發(fā)貢獻(xiàn),版權(quán)歸原作者所有,阿里云開(kāi)發(fā)者社區(qū)不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。具體規(guī)則請(qǐng)查看《阿里云開(kāi)發(fā)者社區(qū)用戶服務(wù)協(xié)議》和《阿里云開(kāi)發(fā)者社區(qū)知識(shí)產(chǎn)權(quán)保護(hù)指引》。如果您發(fā)現(xiàn)本社區(qū)中有涉嫌抄襲的內(nèi)容,填寫(xiě)侵權(quán)投訴表單進(jìn)行舉報(bào),一經(jīng)查實(shí),本社區(qū)將立刻刪除涉嫌侵權(quán)內(nèi)容。總結(jié)
以上是生活随笔為你收集整理的如何实现Java类隔离加载?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 「禾连健康」轻松实现弹性降本20%以上,
- 下一篇: 2020年度盘点出炉!技术好文一口气读完