JAVA類加載的故事二:雙親委派機(jī)制
@[toc] == 我是樓蘭, 你的神秘技術(shù)寶藏 ==
書(shū)接上回
?在我們上一集的故事中,我們?nèi)f能的程序員在邪惡經(jīng)理的壓迫下,實(shí)現(xiàn)了java的熱加載,并且通過(guò)對(duì)類文件的二進(jìn)制流進(jìn)行處理,也保證了從網(wǎng)絡(luò)加載jar包的數(shù)據(jù)安全性。我們?nèi)f能的程序員又花了點(diǎn)功夫,對(duì)這個(gè)機(jī)制的各種異常情況以及打包機(jī)制進(jìn)行了一下修復(fù)完善,很快就實(shí)現(xiàn)出了一整套計(jì)算規(guī)則實(shí)時(shí)發(fā)布的機(jī)制,經(jīng)理用著也很順手。于是,雙方的故事進(jìn)入了短暫的和平期。直到有一天......
第一章、到底加載的是哪個(gè)計(jì)算類?
?在我們之前的簡(jiǎn)單Demo中,薪資計(jì)算類SalaryCaler是必須要放在ClassLoadDemo工程外的。但是我們?nèi)f能的程序員在一次調(diào)試時(shí),不小心在主工程ClassLoadDemo中也新增了一個(gè)薪資計(jì)算類,在提交代碼時(shí)忘了刪除,這種情況在所難免對(duì)吧,因?yàn)樵陂_(kāi)發(fā)過(guò)程中,總是需要在本地進(jìn)行調(diào)試的。在這個(gè)計(jì)算類中,是按照應(yīng)得工資進(jìn)行正確計(jì)算的:?public class SalaryCaler {
? public Double cal(Double salary) {
? ?return salary*0.8;
? }
?}
這樣,整個(gè)代碼結(jié)構(gòu)成了這個(gè)樣子:

?而這個(gè)不小心的操作,在某一次系統(tǒng)重啟后經(jīng)理發(fā)現(xiàn),原來(lái)挺厲害的熱加載功能失效了,每次算出來(lái)的工資都是實(shí)際的工資,也就是說(shuō)**加載到的工資計(jì)算類都是ClassLoaderDemo工程中的SalaryCaler類**(大家可以自己測(cè)試驗(yàn)證下)。這下經(jīng)理不高興了,這不是拆我的臺(tái)嗎?趕緊要我們?nèi)f能的程序員想辦法,徹底解決這個(gè)問(wèn)題。
?
?于是程序員趕緊查百度,想辦法,最終找打了一個(gè)簡(jiǎn)單的方法解決了這個(gè)問(wèn)題。我們的類加載器不會(huì)關(guān)ClassLoadDemo工程中的SalaryCalerl了,而是每次都會(huì)去重新加載目標(biāo)目錄中的類。就是在我們自定義的類加載器中重載一個(gè)父類的方法:? @Override
? public Class<?> loadClass(String name) throws ClassNotFoundException {
? ?if(name.startsWith("com.roy")) {
? ? return this.findClass(name);
? ?}else {
? ? return super.loadClass(name);
? ?}
? }?把這個(gè)方法寫(xiě)到SalaryJARLoader中,這樣,我們?cè)谟?jì)算工資時(shí)就會(huì)加載到j(luò)ar包中的SalaryCaler類,經(jīng)理的問(wèn)題就這樣愉快的解決了。
?
?這是為什么呢?程序員又下了一番苦功夫,才了解到,這是通過(guò)打破JAVA的雙親委派機(jī)制,讓類加載器自行加載外部的class類來(lái)實(shí)現(xiàn)的。但是這到底是怎么回事?別急,我們會(huì)慢慢把這故事給講清楚。
?
?另外,現(xiàn)在我們自定義的這個(gè)類加載器雖然是正確的完成了工作,但是,**把com.roy這樣的報(bào)名直接給寫(xiě)到代碼里,這樣確實(shí)是太low了。**接下來(lái),我們肯定還是要想辦法把com.roy這樣的字眼從代碼中踢出去,同時(shí)**也讓我們的類加載器加載到我們想要的SalaryCaler類,而不用通過(guò)反射來(lái)進(jìn)行計(jì)算**。這樣太別扭了。那帶著這兩個(gè)問(wèn)題,我們稍微深入下JDK源碼,來(lái)找找有什么好的辦法。
JDK類加載核心-雙親委派機(jī)制
?既然我們重寫(xiě)了父類的loadClass才解決的這個(gè)問(wèn)題,那我們當(dāng)然要先看看這個(gè)loadClass方法到底是怎么樣的。我們簡(jiǎn)單跟蹤下源碼,跟到j(luò)ava.lang.ClassLoader中,我們就可以找到這個(gè)方法了。?public Class<?> loadClass(String name) throws ClassNotFoundException {
? ? ? ? ?return loadClass(name, false);
? ? ?}
?protected Class<?> loadClass(String name, boolean resolve)
? ? ? ? ?throws ClassNotFoundException
? ? ?{
? ? ? ? ?synchronized (getClassLoadingLock(name)) {
? ? ? ? ? ? ?<1>
? ? ? ? ? ? ?Class<?> c = findLoadedClass(name);
? ? ? ? ? ? ?<2>
? ? ? ? ? ? ?if (c == null) {
? ? ? ? ? ? ? ? ?long t0 = System.nanoTime();
? ? ? ? ? ? ? ? ?try {
? ? ? ? ? ? ? ? ? ? ?if (parent != null) {
? ? ? ? ? ? ? ? ? ? ? ? ?c = parent.loadClass(name, false);
? ? ? ? ? ? ? ? ? ? ?} else {
? ? ? ? ? ? ? ? ? ? ? ? ?c = findBootstrapClassOrNull(name);
? ? ? ? ? ? ? ? ? ? ?}
? ? ? ? ? ? ? ? ?} catch (ClassNotFoundException e) {
? ? ? ? ? ? ? ? ?}
? ? ? ? ? ? ? ? ?if (c == null) {
? ? ? ? ? ? ? ? ? ? ?long t1 = System.nanoTime();
? ? ? ? ? ? ? ? ? ? ?c = findClass(name);
? ? ? ? ? ? ? ? ? ? ?sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
? ? ? ? ? ? ? ? ? ? ?sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
? ? ? ? ? ? ? ? ? ? ?sun.misc.PerfCounter.getFindClasses().increment();
? ? ? ? ? ? ? ? ?}
? ? ? ? ? ? ?}
? ? ? ? ? ? ?<3>
? ? ? ? ? ? ?if (resolve) {
? ? ? ? ? ? ? ? ?resolveClass(c);
? ? ? ? ? ? ?}
? ? ? ? ? ? ?return c;
? ? ? ? ?}
? ? ?}
我們簡(jiǎn)單的來(lái)看下這個(gè)源碼,下面的每個(gè)點(diǎn)都針對(duì)筆記中的<>部分,然后這一部分最好對(duì)照下我們上一期的雙親委派的那個(gè)圖來(lái)看。這里就是雙親委派機(jī)制的核心。
<1> :這個(gè)findLoadedClass最終會(huì)調(diào)到一個(gè)findLoadedClass0的本地方法,但是從字面意義是不是就能看到這是在查找已經(jīng)加載過(guò)的類?雖然我們暫時(shí)無(wú)法深入C++源碼了解findLoadedClass0的這個(gè)本地方法是如何實(shí)現(xiàn)的,但是至少我們知道了每個(gè)加載器對(duì)他加載的類是有記錄的。
<2>:這一部分其實(shí)就是雙親委派的核心。當(dāng)類加載判斷這個(gè)類自己沒(méi)有加載過(guò)時(shí),就會(huì)到父類加載器中去看父類加載器是否加載過(guò)。一直找到BootStrapClassLoader(這個(gè)類在JAVA中是個(gè)null)。最終如果父加載器中都找不到,就會(huì)調(diào)用自己的findClass方法來(lái)定義這個(gè)類。
<3>:我們看到這里有個(gè)resolve參數(shù),默認(rèn)是false,這個(gè)是干嘛的?這就是我們下面要提到的java類加載過(guò)程。
一個(gè)類的類加載過(guò)程通常分為 **加載、連接、初始化** 三個(gè)部分,具體的行為在java虛擬機(jī)規(guī)范中都有詳細(xì)的定義,這里只是大致的說(shuō)明一下。
加載Loading: 這個(gè)過(guò)程是Java將字節(jié)碼數(shù)據(jù)從不同的數(shù)據(jù)源讀取到JVM中,并映射成為JVM認(rèn)可的數(shù)據(jù)結(jié)構(gòu)。而如果輸入的Class不符合JVM的規(guī)范,就會(huì)拋出異常。這個(gè)階段是用戶可以參與的階段,我們自定義的類加載器,就是工作在這個(gè)過(guò)程。
連接Linking:這個(gè)是核心的步驟,其實(shí)也就是我們上面代碼<3>的這一部分。又可以大致分為三個(gè)小階段:1、驗(yàn)證:檢查JVM加載的字節(jié)信息是否符合Java虛擬機(jī)規(guī)范,否則就會(huì)報(bào)錯(cuò)。這一階段是JVM的安全大門,防止黑客大神的惡意信息或者不合規(guī)信息危害JVM的正常運(yùn)行。2、準(zhǔn)備:這一階段創(chuàng)建類或接口的靜態(tài)變量,并給這些靜態(tài)變量賦一個(gè)初始值(不是最終指定的值),這一部分的作用更大的是預(yù)分配內(nèi)存。3、解析:這一步主要是將常量池中的符號(hào)引用替換為直接引用。例如我們有個(gè)類A調(diào)用了類B的方法,這些在代碼層次還好只是一些對(duì)計(jì)算機(jī)沒(méi)有意義的符號(hào)引用,在這一階段就會(huì)轉(zhuǎn)換成計(jì)算機(jī)所能理解的堆棧、引用等這些直接引用。
初始化Initialization:這一步才是真正去執(zhí)行類初始化的代碼邏輯。包括執(zhí)行static靜態(tài)代碼塊,給靜態(tài)變量賦值等。
這就是整個(gè)類加載最為核心的雙親委派模型。
一不小心打破了雙親委派?
這樣,我們稍微深入了一下JDK的源碼,弄明白了雙親委派機(jī)制。這個(gè)雙親委派機(jī)制對(duì)于保護(hù)JAVA正常運(yùn)行是至關(guān)重要的對(duì)吧。要是沒(méi)有這個(gè)機(jī)制,JDK的底層會(huì)被上層應(yīng)用攪得天翻地覆,也會(huì)讓一些不懷好意的黑客有了隨意注入非法代碼的機(jī)會(huì)。 而再回頭看看我們的SalaryJARClassloader,好像我們通過(guò)復(fù)寫(xiě)父類的loadClass方法,就讓com.roy這個(gè)包名跳出了雙親委派機(jī)制了。這個(gè)包下的類不再去檢查父類是否已經(jīng)加載過(guò),而是直接由我們自定義的類加載器加載了。我們竟然就這樣打破了JDK最為基礎(chǔ)重要的雙親委派?有沒(méi)有感覺(jué)我們一不小心撼動(dòng)了整個(gè)地球? 但是有沒(méi)有覺(jué)得這樣怪怪的?JVM里有兩個(gè)同樣的SalaryCaler類?而且這對(duì)我們需要將com.roy包從代碼里面移除這個(gè)問(wèn)題有什么幫助呢?貌似目前還真沒(méi)有。那有什么別的辦法?別急,我們繼續(xù)來(lái)看看萬(wàn)能程序員的故事。
第二章、消滅com.roy硬編碼,實(shí)現(xiàn)同類多版本加載
故事到這里,我們程序員感覺(jué)已經(jīng)可以自己控制SalaryCaler的加載過(guò)程了。于是我們的程序員就開(kāi)始膨脹了。這經(jīng)理暗地里克扣工資,我要曝光他。于是,我們的萬(wàn)能程序員想要把實(shí)際工資和到手的工資都打印出來(lái)。于是,我們就會(huì)有這樣的代碼: package com.roy; public class SalaryCalDemo2 { public static void main(String[] args) throws Exception { Double salary = 1999.99; SalaryCaler cal1,cal2; while(true) { cal1 = getLocalCaler(); System.out.println("實(shí)際到手Money:"+cal1.cal(salary)); cal2 = getAppCaler(); System.out.println("應(yīng)得的Money:"+cal2.cal(salary)); Thread.sleep(1000); } } //加載自定義的計(jì)算類 private static SalaryCaler getAppCaler() throws Exception { String jarPath = "D:/lib/SalaryCaler.jar"; SalaryJARLoader classloader = new SalaryJARLoader(jarPath); Class<?> objClass = classloader.loadClass("com.roy.SalaryCaler"); Object obj = objClass.newInstance(); return (SalaryCaler)obj; } //加載應(yīng)用中的計(jì)算類 private static SalaryCaler getLocalCaler() throws Exception { return new SalaryCaler(); ? ? ? ? //JVM默認(rèn)都是使用的appClassLoader,并且會(huì)在啟動(dòng)時(shí)將這個(gè)類加載器放入線程向下文當(dāng)中。所以return new SalaryCaler等價(jià)于下面這段代碼。 // ClassLoader appClassLoader= Thread.currentThread().getContextClassLoader(); // Class<?> objClass = appClassLoader.loadClass("com.roy.SalaryCaler"); // Object obj = objClass.newInstance(); // return (SalaryCaler)obj; } } 這個(gè)代碼容易理解把。萬(wàn)能的程序員想要通過(guò)getAppCaler()方法,返回使用自定義類加載器從jar包加載出來(lái)的克扣工資的計(jì)算類。然后通過(guò)getLocalCaler()獲得從JVM默認(rèn)的AppClassLoader從主工程ClassLoadDemo中加載到的展示原價(jià)的工資計(jì)算類。這兩個(gè)類都是同一個(gè)類,通過(guò)我們之前打破雙親委派的機(jī)制,都加載到了JVM內(nèi)存中,通過(guò)這種方式,想要把實(shí)際工資和到手的工資都打出來(lái)。
實(shí)際上這才是我們?cè)诠こ添?xiàng)目中應(yīng)該要用到的設(shè)計(jì)方式對(duì)吧。通過(guò)聲明出不同的SalaryCaler實(shí)現(xiàn)類來(lái)控制計(jì)算邏輯,而把整個(gè)業(yè)務(wù)流程固定下來(lái)。
關(guān)于這個(gè)場(chǎng)景,可以聯(lián)系上JAVA中經(jīng)典的JDBC連接數(shù)據(jù)庫(kù)的兩行代碼。Class.forName()后,DriverManger.getConnection就可以加載出針對(duì)不同數(shù)據(jù)庫(kù)實(shí)現(xiàn)的Conneection。有興趣的可以深入了解下源碼,這是一個(gè)經(jīng)典的面試題。
程序員就這樣打著自己的如意小算盤(pán),運(yùn)行了這段代碼。但是結(jié)果卻遇到下面一個(gè)神奇的問(wèn)題: 實(shí)際到手Money:1999.99 重新加載類:?jar:file:/D:/lib/SalaryCaler.jar!/com/roy/SalaryCaler.class Exception in thread "main" java.lang.ClassCastException: com.roy.SalaryCaler cannot be cast to com.roy.SalaryCaler at com.roy.SalaryCalDemo2.getAppCaler(SalaryCalDemo2.java:22) at com.roy.SalaryCalDemo2.main(SalaryCalDemo2.java:11) 這是啥?SalaryCaler無(wú)法轉(zhuǎn)成SalaryCaler?我是誰(shuí)?誰(shuí)是我? 程序員心中開(kāi)始有點(diǎn)萬(wàn)馬奔騰了。但是,半途而廢又不是他的風(fēng)格,沒(méi)辦法,只好靜下心來(lái)好好分析下這段代碼。
首先分析下報(bào)錯(cuò)的原因。這里就涉及到了JVM類加載的另一個(gè)重要機(jī)制:命名空間隔離機(jī)制。所有的類加載器都會(huì)以當(dāng)前類加載器以及所有的父加載器為要素,形成一個(gè)命名空間。不同命名空間的類是相互不可見(jiàn)的,也就當(dāng)然是不能相互轉(zhuǎn)換的。所以,在我們打破了雙親委派機(jī)制后,盡管我們的JVM內(nèi)存中確實(shí)有兩個(gè)com.roy.SalaryCaler薪資計(jì)算類,但是他們的命名空間是不一樣的,所以也就不能相互轉(zhuǎn)換。
然后我們來(lái)整理下現(xiàn)在遇到的問(wèn)題:目前我們遇到的問(wèn)題根源在于目前我們寫(xiě)在代碼里的SalaryCaler類實(shí)際上都會(huì)被映射成AppClassLoader類加載器加載到的計(jì)算類。而AppClassLoader類中的SalaryCaler只可能有一個(gè)實(shí)現(xiàn)類,所以我們沒(méi)辦法將其他類加載器加載到的SalaryCaler轉(zhuǎn)換成AppClassLoader類加載器中的SalaryCaler。并且,在同一個(gè)AppClassLoader類加載的命名空間中,也是不會(huì)允許同時(shí)有兩個(gè)SalaryCaler類存在的。那我們要實(shí)現(xiàn)多版本,怎么辦呢?那就只有一種迂回的方式,用接口實(shí)現(xiàn)多態(tài)。
具體方案是這樣: 在主工程的ClassLoadDemo中定義一個(gè)SalaryService的計(jì)算類服務(wù)接口。然后,將兩種不同的計(jì)算薪水的服務(wù)打成兩個(gè)jar包,放到不同的目錄。然后給每個(gè)目錄定義一個(gè)SalaryJARLoader。這樣兩個(gè)不同版本的實(shí)現(xiàn)類就可以保存在兩個(gè)不同的SalaryJARLoader類加載器的命名空間里。我們?cè)傧朕k法在ClassLoadDemo主工程的main方法中(都是由AppClassLoader類加載器加載),去分別獲取兩個(gè)SalaryJARLoader里的實(shí)現(xiàn)類,這樣就迂回的實(shí)現(xiàn)了多個(gè)版本同時(shí)存在了。
這個(gè)場(chǎng)景是不是有點(diǎn)類似于Tomcat?Tomcat可以在webapps目錄下部署多個(gè)應(yīng)用。而多個(gè)應(yīng)用之間肯定存在很多類名相同,但是版本不同的類,例如不同版本的Spring類庫(kù)。那在Tomcat的JVM內(nèi)存中,是不是也要保存同一個(gè)類的多個(gè)版本?只有這樣才能保證每個(gè)應(yīng)用的訪問(wèn)是隔離的。
整體方案大致是這樣的:

?那具體怎么實(shí)現(xiàn)呢?我們要按照以下的步驟逐一開(kāi)始改造。
1- 在ClassLoadDemo中增加一個(gè)SalaryCalService接口:
package com.roy.spi; public interface SalaryCalService { public Double cal(Double money); }
2- 在SalaryCaler下先增加一個(gè)按原價(jià)進(jìn)行計(jì)算的實(shí)現(xiàn)類:
package com.roy.spi; public class SalaryCalServiceImpl implements SalaryCalService{ @Override public Double cal(Double money) { System.out.println("Original Service"); return money; } }
注:為了讓代碼能夠編譯通過(guò),我們需要把接口類也拷貝到SalaryCaler工程下,但是我們后面打包不需要用到。也就是maven的provided模式。
3- 然后我們導(dǎo)出SalaryCaler.jar包。 注意,我們導(dǎo)出時(shí),只要導(dǎo)出SalaryCalServiceImpl實(shí)現(xiàn)類,不要導(dǎo)出接口類。我們將這個(gè)jar包導(dǎo)出到D:\lib1目錄。

4- 同樣的方式,我們?cè)赟alaryCaler中再導(dǎo)出一個(gè)包含了按八折方式實(shí)現(xiàn)的SalaryCalServiceImpl實(shí)現(xiàn)類的SalaryClaer.jar,我們導(dǎo)出到D:\lib2目錄。
這樣我們的兩個(gè)jar包都有了。就開(kāi)始玩類加載了。
5- 然后,我們需要對(duì)SalaryJARLoader的loadClass方法進(jìn)行修改,讓他優(yōu)先自己加載類,加載不到的(jar包中沒(méi)有的),再交由父加載器去用雙親委派機(jī)制進(jìn)行加載。
package com.roy; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.URL; import java.security.SecureClassLoader; /** ?* 從jar包中加載薪水計(jì)算類。 ?* @author 樓蘭 ?* ?*/ public class SalaryJARLoader extends SecureClassLoader { private String jarPath; public SalaryJARLoader(String jarPath) { this.jarPath = jarPath; } //打破雙親委派機(jī)制 @Override public Class<?> loadClass(String name) throws ClassNotFoundException { // if(name.startsWith("com.roy")) { // return this.findClass(name); // }else { // return super.loadClass(name); // } // 把雙親委派機(jī)制反過(guò)來(lái),先到子類加載器中加載,加載不到再去父類加載器中加載。 synchronized (getClassLoadingLock(name)) { Class<?> c; try { // 先由父加載器去加載 c = this.findClass(name); } catch (ClassNotFoundException e) { // 父加載器加載不到的jar包中的類,由加載器自己加載。 c = this.getParent().loadClass(name); } return c; } } ? ? //findClass方法不動(dòng)。 }
6- 然后再在ClassLoadDemo中看看我們按自己的反雙親委派的機(jī)制進(jìn)行的類加載器效果怎么樣。在ClassLoadDemo中添加一個(gè)測(cè)試方法:
public class SalaryCalDemo3 { public static void main(String[] args) throws Exception { Double salary = 2000.00; SalaryCalService originalService,discountedService; String originalJarPath = "D:/lib1/SalaryCaler.jar"; String discountedJarPath = "D:/lib2/SalaryCaler.jar"; while(true) { originalService = ?getOriginalService(originalJarPath); System.out.println("應(yīng)得的Money:"+originalService.cal(salary)); discountedService = getOriginalService(discountedJarPath);; System.out.println("實(shí)際到手Money:"+discountedService.cal(salary)); System.out.println("===================="); Thread.sleep(1000); } ? ? } ? ? private static SalaryCalService getOriginalService(String jarPath) throws Exception { ? ? //這里一定要new一個(gè)classLoader來(lái),不可以重復(fù)使用。 ? ? //因?yàn)橥粋€(gè)classloader不可以多次去重新加載service實(shí)現(xiàn)類,會(huì)報(bào)錯(cuò)的。 SalaryJARLoader myclassloader = new SalaryJARLoader(jarPath); return (SalaryCalService)myclassloader.loadClass("com.roy.spi.SalaryCalServiceImpl").newInstance(); } }
7- 運(yùn)行的結(jié)果,可以同時(shí)打印出應(yīng)得的money和實(shí)際到手的money。

階段總結(jié):通過(guò)這一階段的折騰,我們的程序員可以在OA系統(tǒng)中(由主工程ClassLoadDemo來(lái)模擬)獲取到兩個(gè)不同版本的薪水計(jì)算實(shí)現(xiàn)類,并且保存了之前熱加載的功能。也把我們之前在SalaryJARLoader中的com.roy硬編碼問(wèn)題給解決掉了。這樣,我們以讓熱加載機(jī)制加載多一份垃圾對(duì)象的方式,實(shí)現(xiàn)了同類的多個(gè)版本加載。
問(wèn)題看似挺完美。但是還是會(huì)有新的問(wèn)題。我們之前在SalaryJARLoader中寫(xiě)死了com.roy硬編碼方式,但是這么一折騰,雖然SalaryJARLoader中的com.roy沒(méi)了,但是在測(cè)試方法中,加載服務(wù)時(shí),又多出來(lái)了com.roy.spi.SalaryCalServiceImpl這樣的硬編碼。這樣的硬編碼方式會(huì)使得我們?cè)诿看问褂眠@個(gè)服務(wù)時(shí)都需要記住這么長(zhǎng)一串服務(wù)全路徑。是不是依然非常不爽?那接下來(lái),我們?cè)倮^續(xù)消滅這個(gè)長(zhǎng)串的服務(wù)名。
第三章、消滅服務(wù)名硬編碼,實(shí)現(xiàn)SPI服務(wù)發(fā)現(xiàn)
要消滅這種長(zhǎng)串的硬編碼,一般有兩種方式: 一是通過(guò)某些特征來(lái)尋找服務(wù)名,而不用硬編碼指定。常用的特征有使用注解、父類或者泛型等特征。 但是在我們這個(gè)場(chǎng)景上卻用不上。為什呢?我們要注意,我們需要的是在類加載的過(guò)程中去尋找到服務(wù)類。而在類加載的過(guò)程中,其他的類加載是不完全的,所以,注解、泛型這些特征都是不靠譜的。而父類似乎靠譜點(diǎn),但是我們這個(gè)場(chǎng)景中的父類和實(shí)現(xiàn)類是跨類加載器的命名空間的,這就讓尋找父類變得非常困難。所以這種方式我們用不上。
當(dāng)然,這個(gè)結(jié)論也是我們的程序員百般測(cè)試下得出的結(jié)論。大家也可以自己按這個(gè)思路去嘗試一下。
那第二種方式就是把這種硬編碼寫(xiě)到一個(gè)配置文件里去。 因?yàn)榕渲梦募强梢噪S時(shí)修改不用編譯的,放到配置文件里,就可以在JAVA運(yùn)行時(shí),更新配置文件,而不用動(dòng)java代碼,重啟java進(jìn)程。這樣也算是迂回的保住了我們的熱加載機(jī)制。 那我們選定了第二種方法,接下來(lái)就是找實(shí)現(xiàn)的方式了。 就在程序員一籌莫展之時(shí),突然想到,我們?cè)谑褂肑DBC獲取數(shù)據(jù)庫(kù)連接的時(shí)候是不是也有類似的場(chǎng)景?我們?cè)讷@取JDBC連接時(shí),會(huì)使用DriverManager.getConnection方法來(lái)獲取針對(duì)不同數(shù)據(jù)庫(kù)的連接實(shí)現(xiàn)類,然后通過(guò)這個(gè)實(shí)現(xiàn)類來(lái)完成與數(shù)據(jù)庫(kù)的交互。這跟我們要獲得SalaryService的不同實(shí)現(xiàn),不是很相似嗎? 于是,萬(wàn)能的程序員就開(kāi)始深入DriverManager的源碼,尋找答案。 終于,程序員最終在java.sql.DriverManager源代碼的第586行找到了一行神奇的代碼,最終帶他走出了困局。 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
SPI機(jī)制介紹:
既然這行代碼這么神奇,首先要解釋下這行代碼是干什么的。這行代碼就是JDK提供的**SPI機(jī)制**。 這個(gè)**SPI機(jī)制**可以對(duì)一個(gè)接口(Driver.class),加載到他的所有實(shí)現(xiàn)類。加載的方式需要在項(xiàng)目資源目錄下加載一個(gè)META-INF/services/{接口全類名}的文件(這個(gè)文件就在各個(gè)數(shù)據(jù)庫(kù)廠商提供的驅(qū)動(dòng)包里),然后在文件中,一行指定一個(gè)實(shí)現(xiàn)類,可以指定多個(gè)。這樣就可以通過(guò)ServiceLoader的load方法加載出這個(gè)接口的所有實(shí)現(xiàn)類。 SPI機(jī)制可以實(shí)現(xiàn)多類型的加載,另外,他還可以很方便的實(shí)現(xiàn)跨命名空間的類加載。在SerivceLoader中,還有一個(gè)load的重載方法,可以傳入一個(gè)classLoader,這樣我們就可以輕松的讀取其他命名空間下的類信息了。然后,在SerivceLoader的load實(shí)現(xiàn)方法中,還可以看到,他使用了一個(gè)類加載器上下文來(lái)獲取所需要的類加載器。 public static <S> ServiceLoader<S> load(Class<S> service) { ? ? ? ? ClassLoader cl = Thread.currentThread().getContextClassLoader(); ? ? ? ? return ServiceLoader.load(service, cl); ? ? } 而這個(gè)類加載器上下文,會(huì)在JVM啟動(dòng)時(shí)把AppClassLoader給設(shè)置進(jìn)去。具體可以去查查AppClassLoader的源碼。 SPI這種跨類加載器的用法就非常適合我們這個(gè)場(chǎng)景了,在我們這個(gè)場(chǎng)景下,需要切換不同的SalaryJARClassloader實(shí)例來(lái)獲取不同的實(shí)現(xiàn)類版本,就可以用SPI機(jī)制來(lái)幫忙。
動(dòng)手改造
了解這個(gè)機(jī)制后,就開(kāi)始對(duì)我們的工程進(jìn)行進(jìn)一步改造了。 首先,服務(wù)接口類,我們已經(jīng)有了,就用SalaryCalService。 其次,服務(wù)實(shí)現(xiàn)類的類名,這個(gè)我們也已經(jīng)確定下來(lái)了,就是我們需要消滅掉的com.roy.spi.SalaryCalServiceImpl這一串。 然后,就可以按SPI機(jī)制來(lái)寫(xiě)配置文件了。 在ClassLoader工程下新增一個(gè)名為resources的源文件夾source folder,然后在下面新增META-INF/services/com.roy.spi.SalaryCalService文件。然后在文件里,只要寫(xiě)一行內(nèi)容: com.roy.spi.SalaryCalServiceImpl 這樣,SPI的環(huán)境配置就配好了。整體代碼像這樣:

?然后我們就可以開(kāi)始對(duì)測(cè)試方法進(jìn)行改造了。
?package com.roy;
?
?import java.util.Iterator;
?import java.util.ServiceLoader;
?
?import com.roy.spi.SalaryCalService;
?
?/**
? * 使用SPI機(jī)制實(shí)現(xiàn)多版本加載
? *
? * @author 樓蘭
? */
?public class SalaryCalDemo4 {
?
? public static void main(String[] args) throws Exception {
? ?Double salary = 2000.00;
?
? ?SalaryCalService originalService, discountedService;
? ?String originalJarPath = "D:/lib1/SalaryCaler.jar";
? ?String discountedJarPath = "D:/lib2/SalaryCaler.jar";
?
? ?while (true) {
? ? originalService = getOriginalService(originalJarPath);
? ? System.out.println("應(yīng)得的Money:" + originalService.cal(salary));
? ? discountedService = getOriginalService(discountedJarPath);
? ? ;
? ? System.out.println("實(shí)際到手Money:" + discountedService.cal(salary));
?
? ? System.out.println("====================");
? ? Thread.sleep(1000);
?
? ?}
? }
?
? private static SalaryCalService getOriginalService(String jarPath) throws Exception {
? ?// 這里一定要new一個(gè)classLoader來(lái),不可以重復(fù)使用。
? ?// 因?yàn)橥粋€(gè)classloader不可以多次去重新加載service實(shí)現(xiàn)類,會(huì)報(bào)錯(cuò)的。
? ?SalaryJARLoader myclassloader = new SalaryJARLoader(jarPath);
? ?Iterator<SalaryCalService> iter = ServiceLoader.load(SalaryCalService.class,myclassloader).iterator();
? ?if (iter.hasNext()) {
? ? // 只要一個(gè)子類
? ? return iter.next();
? ?} else {
? ? throw new ClassNotFoundException("缺少SPI的實(shí)現(xiàn)類");
? ?}
? ?//上面是比較簡(jiǎn)單的用法,常用的是下面這種方法,減少上下文的切換。
?// ?ClassLoader classloader = Thread.currentThread().getContextClassLoader();
?// ?try {
?// ? Thread.currentThread().setContextClassLoader(classloader);
?// ? Iterator<SalaryCalService> iter = ServiceLoader.load(SalaryCalService.class).iterator();
?// ? if(iter.hasNext()) {
?// ? ?//只要一個(gè)子類
?// ? ?return iter.next();
?// ? }else {
?// ? ?throw new ClassNotFoundException("缺少SPI的實(shí)現(xiàn)類");
?// ? }
?// ?}finally {
?// ? Thread.currentThread().setContextClassLoader(classloader);
?// ?}
? }
?}?代碼測(cè)試的結(jié)果還是能正常打印出實(shí)際工資和克扣后的工資,就不再演示了。
故事完結(jié)
?我們這一期的故事暫告一段落,我們的程序員簡(jiǎn)單的進(jìn)入了一下JDK源碼,就給自己引出了一大堆的麻煩。例如我們這兩期的這個(gè)熱加載機(jī)制,其實(shí)可以想象,在后臺(tái)創(chuàng)建了大量的垃圾對(duì)象??梢韵胂螅?dāng)系統(tǒng)稍微變復(fù)雜一點(diǎn),需要占用更多的資源,那這些垃圾對(duì)象就會(huì)造成非常大的影響。那影響到底會(huì)怎么樣?要怎么去評(píng)估?怎么去優(yōu)化?有沒(méi)有感興趣的朋友?可以在下面留言,看能不能有下一期的故事。