最近一個項目需求痛侍,實現基于http接口的外部jar包動態(tài)類加載朝氓。我平臺提供標準化的接口,接口的具體實現由業(yè)務方實現主届。業(yè)務方根據開發(fā)規(guī)范赵哲,實現接口后,打包成jar文件君丁,上傳至平臺上枫夺,用戶調用接口的時候,動態(tài)載入jar文件绘闷,運行結果返回橡庞。整個過程,業(yè)務方開發(fā)人員通過平臺的管理頁面配置,并上傳實現的jar包,即可把能力添加到我平臺上。
??整個項目的一個關鍵點事如何動態(tài)加載類文件,還必須實現動態(tài)更新虫溜。
??基于這一目標,對java類加載做了具體學習键袱。
類的裝載方式
首先我們來認識一下ClassLoader鹉动,程序在啟動的時候再菊,并不會一次性加載程序所要用的所有class文件,而是根據程序的需要颜曾,通過Java的類加載機制(ClassLoader)來動態(tài)加載某個 class 文件到內存當中的纠拔,從而只有 class 文件被載入到了內存之后,才能被其它 class 所引用泛豪。所以 ClassLoader 就是用來動態(tài)加載 class 文件到內存當中用的稠诲。
ClassLoader主要對類的請求提供服務,當JVM需要某類時诡曙,它根據名稱向ClassLoader要求這個類臀叙,然后由ClassLoader返回這個類的class對象。
Classloader就是負責將class加載到JVM中价卤,基于某個加載模型確定到底由誰加載劝萤,最后將 Class 字節(jié)碼重新解析成 JVM 統(tǒng)一要求的對象格式。
JAVA類裝載方式慎璧,有兩種:
1.隱式裝載床嫌,程序在運行過程中當碰到通過new 等方式生成對象時,隱式調用類裝載器加載對應的類到jvm中胸私。
2.顯式裝載厌处,通過class.forname(),this.getClassLoader().loadClass()等方法岁疼,顯式加載需要的類阔涉。
類的裝載過程
看一下ClassLoader中l(wèi)oadClass方法的實現:
1.調用findLoaderClass查看是否已存在裝入的類。存在就直接返回class對象(java中任何類型都有對應的class對象捷绒,哪怕是void也有)瑰排。
2.findSystemClass查找本地文件系統(tǒng),根據不同的類裝載器疙驾,文件系統(tǒng)位置不一樣凶伙。沒有找到則返回ClassNotFoundException.
3.difineClass 接收以字節(jié)數組表示的類字節(jié)碼,并把它轉換成 Class 實例
4.resolveClass 鏈接一個指定的類它碎。(jvm裝載類的三個過程為load ,link,initialize)
loadClass方法的默認實現按照以下順序查找類: 調用findLoadedClass(String) 方法檢查這個類是否被加載過 使用父加載器調用 loadClass(String) 方法函荣,如果父加載器為 Null,類加載器裝載虛擬機內置的加載器調用 findClass(String) 方法裝載類扳肛, 如果傻挂,按照以上的步驟成功的找到對應的類,并且該方法接收的 resolve 參數的值為 true,那么就調用resolveClass(Class) 方法來處理類挖息。
ClassLoader 的子類最好覆蓋 findClass(String) 而不是這個方法金拒。 除非被重寫,這個方法默認在整個裝載過程中都是同步的(線程安全的)。
類裝載器
ClassLoader 在加載類時有一定的層次關系和規(guī)則绪抛,樹狀組織結構资铡。在 Java 中,有四種類型的類加載器幢码,分別為:BootStrapClassLoader笤休、ExtClassLoader、AppClassLoader 以及用戶自定義的 ClassLoader
1.BootStrapClassLoader(引導類加載器) 處于類加載器層次結構的最高層症副,負責 sun.boot.class.path 路徑下類的加載店雅,默認為 jre/lib 目錄下的核心 API 或 -Xbootclasspath 選項指定的 jar 包。
2.ExtClassLoader (擴展類加載器)的加載路徑為 java.ext.dirs贞铣,默認為 jre/lib/ext 目錄或者 -Djava.ext.dirs 指定目錄下的 jar 包加載闹啦。
3.AppClassLoader(系統(tǒng)類加載器) 的加載路徑為 java.class.path,默認為環(huán)境變量 CLASSPATH 中設定的值辕坝。也可以通過 -classpath 選型進行指定窍奋。
4.用戶自定義 ClassLoader 可以根據用戶的需要定制自己的類加載過程,在運行期進行指定類的動態(tài)實時加載圣勒。
一般來說费变,這四種類加載器會形成一種父子關系,高層為低層的父加載器圣贸。在進行類加載時挚歧,首先會自底向上挨個檢查是否已經加載了指定類,如果已經加載則直接返回該類的引用吁峻。如果到最高層也沒有加載過指定類滑负,那么會自頂向下挨個嘗試加載,直到用戶自定義類加載器用含,如果還不能成功矮慕,就會拋出異常。
對于同一個類加載器實例來說啄骇,名字相同的類只能存在一個痴鳄,并且僅加載一次。不管該類有沒有變化缸夹,下次再需要加載時痪寻,它只是從自己的緩存中直接返回已經加載過的類引用。
類加載原理——雙親委托模型
每個ClassLoader實例都有一個父類加載器的引用(不是繼承的關系虽惭,是一個包含的關系)橡类,虛擬機內置的類加載器(Bootstrap ClassLoader)本身沒有父類加載器,但可以用作其它ClassLoader實例的的父類加載器芽唇。
??當一個ClassLoader實例需要加載某個類時顾画,它會試圖親自搜索某個類之前,先把這個任務委托給它的父類加載器,這個過程是由上至下依次檢查的研侣,首先由最頂層的類加載器Bootstrap ClassLoader試圖加載谱邪,如果沒加載到,則把任務轉交給Extension ClassLoader試圖加載庶诡,如果也沒加載到虾标,則轉交給App ClassLoader 進行加載,如果它也沒有加載得到的話灌砖,則返回給委托的發(fā)起者,由它到指定的文件系統(tǒng)或網絡等URL中加載該類傀蚌。
??如果它們都沒有加載到這個類時基显,則拋出ClassNotFoundException異常。否則將這個找到的類生成一個類的定義善炫,并將它加載到內存當中撩幽,最后返回這個類在內存中的Class實例對象。
為什么要使用雙親委托這種模型呢箩艺?
因為這樣可以避免重復加載窜醉,當父親已經加載了該類的時候,就沒有必要 ClassLoader再加載一次艺谆≌ザ瑁考慮到安全因素,我們試想一下静汤,如果不使用這種委托模式琅催,那我們就可以隨時使用自定義的String來動態(tài)替代java核心api中定義的類型,這樣會存在非常大的安全隱患虫给,而雙親委托的方式藤抡,就可以避免這種情況,因為String已經在啟動時就被引導類加載器(Bootstrcp ClassLoader)加載抹估,所以用戶自定義的ClassLoader永遠也無法加載一個自己寫的String缠黍,除非你改變JDK中ClassLoader搜索類的默認算法。
但是JVM在搜索類的時候药蜻,又是如何判定兩個class是相同的呢瓷式?
JVM在判定兩個class是否相同時,不僅要判斷兩個類名是否相同谷暮,而且要判斷是否由同一個類加載器實例加載的蒿往。只有兩者同時滿足的情況下,JVM才認為這兩個class是相同的湿弦。就算兩個class是同一份class字節(jié)碼瓤漏,如果被兩個不同的ClassLoader實例所加載,JVM也會認為它們是兩個不同class。
Java裝載類使用“全盤負責委托機制”蔬充〉悖“全盤負責”是指當一個ClassLoder裝載一個類時,該類所依賴及引用的類也由這個ClassLoder載入(除非顯示的使用另外一個ClassLoder)饥漫;“委托機制”是指先委托父類裝載器尋找目標類榨呆,只有在找不到的情況下才從自己的類路徑中查找并裝載目標類。
線程上下文類加載器
每個運行中的線程都有一個成員ContextClassLoader庸队,用來在運行時動態(tài)地載入其它類积蜻,可以使用方法Thread.currentThread().setContextClassLoader(...);更改當前線程的contextClassLoader,來改變其載入類的行為彻消;也可以通過方法Thread.currentThread().getContextClassLoader()來獲得當前線程的ClassLoader竿拆。
??Java 應用運行的初始線程的上下文類加載器是系統(tǒng)類加載器。在線程中運行的代碼可以通過此類加載器來加載類和資源宾尚。
實際上丙笋,在Java應用中所有程序都運行在線程里,如果在程序中沒有手工設置過ClassLoader煌贴,對于一般的java類如下兩種方法獲得的ClassLoader通常都是同一個御板。
this.getClass.getClassLoader();
Thread.currentThread().getContextClassLoader();
方法一得到的Classloader是靜態(tài)的,表明類的載入者是誰;
方法二得到的Classloader是動態(tài)的牛郑,誰執(zhí)行(某個線程)怠肋,就是那個執(zhí)行者的Classloader。
對于單例模式的類井濒,靜態(tài)類等灶似,載入一次后,這個實例會被很多程序(線程)調用瑞你,對于這些類酪惭,載入的Classloader和執(zhí)行線程的Classloader通常都不同。
WEB容器類加載器
對于運行在 Java EE容器中的 Web 應用來說者甲,類加載器的實現方式與一般的 Java 應用有所不同春感。不同的 Web 容器的實現方式也會有所不同。
??以 Apache Tomcat 來說虏缸,每個 Web 應用都有一個對應的類加載器實例鲫懒。該類加載器也使用代理模式,所不同的是它是首先嘗試去加載某個類刽辙,如果找不到再代理給父類加載器窥岩。這與一般類加載器的順序是相反的。這是 Java Servlet 規(guī)范中的推薦做法宰缤,其目的是使得 Web 應用自己的類的優(yōu)先級高于 Web 容器提供的類颂翼。
??這種代理模式的一個例外是:Java 核心庫的類是不在查找范圍之內的晃洒。這也是為了保證 Java 核心庫的類型安全。
對于WEB APP線程朦乏,它的contextClassLoader是WebAppClassLoader
對于Tomcat Server線程球及,它的contextClassLoader是CatalinaClassLoader
自定義類加載器
默認的classloader只能加載指定目錄下的jar和class,如果想加載其它位置的類或jar時呻疹,需要定義自己的classloader
定義自已的類加載器分為兩步:
1吃引、繼承java.lang.ClassLoader
2、重寫父類的findClass方法
文件類加載器
??加載存儲在文件系統(tǒng)上的 Java 字節(jié)代碼刽锤。
??類 FileSystemClassLoader繼承自類java.lang.ClassLoader镊尺。為了保證類加載器都正確實現代理模式,在開發(fā)自己的類加載器時并思,最好不要覆寫 loadClass()方法鹅心,而是覆寫findClass()方法。
??類 FileSystemClassLoader的 findClass()方法首先根據類的全名在硬盤上查找類的字節(jié)代碼文件(.class 文件)纺荧,然后讀取該文件內容,最后通過 defineClass()方法來把這些字節(jié)代碼轉換成 java.lang.Class類的實例颅筋。
public class FileSystemClassLoader extends ClassLoader
{
private String rootDir;
public FileSystemClassLoader(String rootDir){
this.rootDir = rootDir;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null){
throw new ClassNotFoundException();
}
else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
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 null;
}
private String classNameToPath(String className) {
return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
}
}
網絡類加載器
??一個網絡類加載器來說明如何通過類加載器來實現組件的動態(tài)更新宙暇。即基本的場景是:Java 字節(jié)代碼(.class)文件存放在服務器上,客戶端通過網絡的方式獲取字節(jié)代碼并執(zhí)行议泵。當有版本更新的時候占贫,只需要替換掉服務器上保存的文件即可。通過類加載器可以比較簡單的實現這種需求先口。
??類 NetworkClassLoader 負責通過網絡下載 Java 類字節(jié)代碼并定義出 Java 類型奥。它的實現與FileSystemClassLoader 類似。在通過 NetworkClassLoader 加載了某個版本的類之后碉京,一般有兩種做法來使用它厢汹。第一種做法是使用 Java 反射 API。另外一種做法是使用接口谐宙。
OSGI動態(tài)模塊系統(tǒng)
osgi是java動態(tài)化模塊系統(tǒng)烫葬,動態(tài)模塊部署。面向服務和基于組件的運行環(huán)境凡蜻。組件盡可能解耦搭综,能讓組件動態(tài)的發(fā)現其他組件。
這個有興趣也可以去研究關注一下划栓。
關于《java類的熱替換》參考:
https://www.ibm.com/developerworks/cn/java/j-lo-hotswapcls/