阿里妹導(dǎo)讀:java 開(kāi)發(fā)中,如果不同的 jar 包依賴了某些通用 jar 包的版本不一樣,運(yùn)行時(shí)就會(huì)因?yàn)榧虞d的類跟預(yù)期不符合導(dǎo)致報(bào)錯(cuò)。如何避免這種情況呢?本文通過(guò)分析 jar 包產(chǎn)生沖突的原因及類隔離的實(shí)現(xiàn)原理,分享兩種實(shí)現(xiàn)自定義類加載器的方法。
一 什么是類隔離技術(shù)
只要你 java 代碼寫的足夠多,就一定會(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,寫代碼五分鐘,排包排了一整天。
上面這種情況就是 java 開(kāi)發(fā)過(guò)程中常見(jiàn)的情況,原因也很簡(jiǎn)單,不同 jar 包依賴了某些通用 jar 包(如日志組件)的版本不一樣,編譯的時(shí)候沒(méi)問(wèn)題,到了運(yùn)行時(shí)就會(huì)因?yàn)榧虞d的類跟預(yù)期不符合導(dǎo)致報(bào)錯(cuò)。舉個(gè)例子:a 和 b 分別依賴了 c 的 v1 和 v2 版本,v2 版本的 log 類比 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)目的所有類都是用同一個(gè)類加載器加載的,所以不管你依賴了多少個(gè)版本的 c,最終只會(huì)有一個(gè)版本的 c 被加載到 jvm 中。當(dāng) b 要去訪問(wèn) log.error,就會(huì)發(fā)現(xiàn) log 壓根就沒(méi)有 error 方法,然后就拋異常java.lang.nosuchmethoderror。這就是類沖突的一個(gè)典型案例。
類沖突的問(wèn)題如果版本是向下兼容的其實(shí)很好解決,把低版本的排除掉就完事了。但要是遇到版本不向下兼容的那就陷入了“救媽媽還是救女朋友”的兩難處境了。
為了避免兩難選擇,有人就提出了類隔離技術(shù)來(lái)解決類沖突的問(wèn)題。類隔離的原理也很簡(jiǎn)單,就是讓每個(gè)模塊使用獨(dú)立的類加載器來(lái)加載,這樣不同模塊之間的依賴就不會(huì)互相影響。如下圖所示,不同的模塊用不同的類加載器加載。為什么這樣做就能解決類沖突呢?這里用到了 java 的一個(gè)機(jī)制:不同類加載器加載的類在 jvm 看來(lái)是兩個(gè)不同的類,因?yàn)樵?jvm 中一個(gè)類的唯一標(biāo)識(shí)是 類加載器+類名。通過(guò)這種方式我們就能夠同時(shí)加載 c 的兩個(gè)不同版本的類,即使它類名是一樣的。注意,這里類加載器指的是類加載器的實(shí)例,并不是一定要定義兩個(gè)不同類加載器,例如圖中的 pluginclassloadera 和 pluginclassloaderb 可以是同一個(gè)類加載器的不同實(shí)例。
二 如何實(shí)現(xiàn)類隔離
前面我們提到類隔離就是讓不同模塊的 jar 包用不同的類加載器加載,要做到這一點(diǎn),就需要讓 jvm 能夠使用自定義的類加載器加載我們寫的類以及其關(guān)聯(lián)的類。
那么如何實(shí)現(xiàn)呢?一個(gè)很簡(jiǎn)單的做法就是 jvm 提供一個(gè)全局類加載器的設(shè)置接口,這樣我們直接替換全局類加載器就行了,但是這樣無(wú)法解決多個(gè)自定義類加載器同時(shí)存在的問(wèn)題。
實(shí)際上 jvm 提供了一種非常簡(jiǎn)單有效的方式,我把它稱為類加載傳導(dǎo)規(guī)則:jvm 會(huì)選擇當(dāng)前類的類加載器來(lái)加載所有該類的引用的類。例如我們定義了 testa 和 testb 兩個(gè)類,testa 會(huì)引用 testb,只要我們使用自定義的類加載器加載 testa,那么在運(yùn)行時(shí),當(dāng) testa 調(diào)用到 testb 的時(shí)候,testb 也會(huì)被 jvm 使用 testa 的類加載器加載。依此類推,只要是 testa 及其引用類關(guān)聯(lián)的所有 jar 包的類都會(huì)被自定義類加載器加載。通過(guò)這種方式,我們只要讓模塊的 main 方法類使用不同的類加載器加載,那么每個(gè)模塊的都會(huì)使用 main 方法類的類加載器加載的,這樣就能讓多個(gè)模塊分別使用不同類加載器。這也是 osgi 和 sofaark 能夠?qū)崿F(xiàn)類隔離的核心原理。
了解了類隔離的實(shí)現(xiàn)原理之后,我們從重寫類加載器開(kāi)始進(jìn)行實(shí)操。要實(shí)現(xiàn)自己的類加載器,首先讓自定義的類加載器繼承 java.lang.classloader,然后重寫類加載的方法,這里我們有兩個(gè)選擇,一個(gè)是重寫 findclass(string name),一個(gè)是重寫 loadclass(string name)。那么到底應(yīng)該選擇哪個(gè)?這兩者有什么區(qū)別?
下面我們分別嘗試重寫這兩個(gè)方法來(lái)實(shí)現(xiàn)自定義類加載器。
1.重寫 findclass
首先我們定義兩個(gè)類,testa 會(huì)打印自己的類加載器,然后調(diào)用 testb 打印它的類加載器,我們預(yù)期是實(shí)現(xiàn)重寫了 findclass 方法的類加載器 myclassloaderparentfirst 能夠在加載了 testa 之后,讓 testb 也自動(dòng)由 myclassloaderparentfirst 來(lái)進(jìn)行加載。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public class testa { public static void main(string[] args) { testa testa = new testa(); testa.hello(); } public void hello() { // https://jinglingwang.cn/archives/class-isolation-loading 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()); } } |
然后重寫一下 findclass 方法,這個(gè)方法先根據(jù)文件路徑加載 class 文件,然后調(diào)用 defineclass 獲取 class 對(duì)象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
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" ); } // 重寫了 findclass 方法 by:jinglingwang.cn @override public 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 = new bytearrayoutputstream()) { 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 [] {}; } } |
最后寫一個(gè) main 方法調(diào)用自定義的類加載器加載 testa,然后通過(guò)反射調(diào)用 testa 的 main 方法打印類加載器的信息。
1
2
3
4
5
6
7
8
|
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é)果如下:
1
2
|
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è)類加載的規(guī)則:jvm 在觸發(fā)類加載時(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 方法類默認(rèn)情況下都是由 jdk 自帶的 appclassloader 加載的,根據(jù)類加載傳導(dǎo)規(guī)則,main 類引用的 myclassloaderparentfirst 也是由加載了 main 類的appclassloader 來(lái)加載。由于 myclassloaderparentfirst 的父類是 classloader,classloader 的默認(rèn)構(gòu)造方法會(huì)自動(dòng)設(shè)置父加載器的值為 appclassloader。
1
2
3
|
protected classloader() { this (checkcreateclassloader(), getsystemclassloader()); } |
2.重寫 loadclass
由于重寫 findclass 方法會(huì)受到雙親委派機(jī)制的影響導(dǎo)致 testb 被 appclassloader 加載,不符合類隔離的目標(biāo),所以我們只能重寫 loadclass 方法來(lái)破壞雙親委派機(jī)制。代碼如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
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" ); } @override protected class <?> loadclass(string name, boolean resolve) throws classnotfoundexception { class result = null ; try { //by:jinglingwang.cn 這里要使用 jdk 的類加載器加載 java.lang 包里面的類 result = jdkclassloader.loadclass(name); } catch (exception e) { //忽略 by:jinglingwang.cn } 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),我們重寫了 loadclass 方法也就是意味著所有類包括 java.lang 包里面的類都會(huì)通過(guò) myclassloadercustom 進(jìn)行加載,但類隔離的目標(biāo)不包括這部分 jdk 自帶的類,所以我們用 extclassloader 來(lái)加載 jdk 的類,相關(guān)的代碼就是:result = jdkclassloader.loadclass(name);
測(cè)試代碼如下:
1
2
3
4
5
6
7
8
9
10
|
public class mytest { public static void main(string[] args) throws exception { //這里取appclassloader的父加載器也就是extclassloader作為myclassloadercustom的jdkclassloader myclassloadercustom 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é)果如下:
1
2
|
testa: com.java.loader.myclassloadercustom @1d44bcfa testb: com.java.loader.myclassloadercustom @1d44bcfa |
可以看到,通過(guò)重寫了 loadclass 方法,我們成功的讓 testb 也使用myclassloadercustom 加載到了 jvm 中。
三 總結(jié)
類隔離技術(shù)是為了解決依賴沖突而誕生的,它通過(guò)自定義類加載器破壞雙親委派機(jī)制,然后利用類加載傳導(dǎo)規(guī)則實(shí)現(xiàn)了不同模塊的類隔離。
以上就是兩種實(shí)現(xiàn)java類隔離加載的方法的詳細(xì)內(nèi)容,更多關(guān)于java類隔離加載的資料請(qǐng)關(guān)注服務(wù)器之家其它相關(guān)文章!