http://blog.csdn.net/zhoudaxia/article/details/35824249
1 基本信息
每個開發(fā)人員對java.lang.ClassNotFoundExcetpion這個異常肯定都不陌生,這背后就涉及到了java技術(shù)體系中的類加載。Java的類加載機(jī)制是技術(shù)體系中比較核心的部分,雖然和大部分開發(fā)人員直接打交道不多,但是對其背后的機(jī)理有一定理解有助于排查程序中出現(xiàn)的類加載失敗等技術(shù)問題,對理解java虛擬機(jī)的連接模型和java語言的動態(tài)性都有很大幫助。
2 Java虛擬機(jī)類加載器結(jié)構(gòu)簡述
2.1 JVM三種預(yù)定義類型類加載器
我們首先看一下 JVM預(yù)定義的三種類型類加載器,當(dāng)一個 JVM啟動的時候, Java缺省開始使用如下三種類型類裝入器:
啟動( Bootstrap)類加載器 :引導(dǎo)類裝入器是用本地代碼實(shí)現(xiàn)的類裝入器,它負(fù)責(zé)將 <Java_Runtime_Home>/lib下面的核心類庫或-Xbootclasspath選項(xiàng)指定的jar包加載到內(nèi)存中。由于引導(dǎo)類加載器涉及到虛擬機(jī)本地實(shí)現(xiàn)細(xì)節(jié),開發(fā)者無法直接獲取到啟動類加載器的引用,所以不允許直接通過引用進(jìn)行操作。
擴(kuò)展( Extension)類加載器 :擴(kuò)展類加載器是由 Sun的 ExtClassLoader( sun.misc.Launcher$ExtClassLoader) 實(shí)現(xiàn)的。它負(fù)責(zé)將 < Java_Runtime_Home >/lib/ext或者由系統(tǒng)變量 -Djava.ext.dir指定位置中的類庫加載到內(nèi)存中。開發(fā)者可以直接使用標(biāo)準(zhǔn)擴(kuò)展類加載器。
系統(tǒng)( System)類加載器 :系統(tǒng)類加載器是由 Sun的 AppClassLoader( sun.misc.Launcher$AppClassLoader)實(shí)現(xiàn)的。它負(fù)責(zé)將系統(tǒng)類路徑 java -classpath或-Djava.class.path變量所指的目錄下的類庫加載到內(nèi)存中。開發(fā)者可以直接使用系統(tǒng)類加載器。
除了以上列舉的三種類加載器,還有一種比較特殊的類型就是線程上下文類加載器 ,這個將在后面單獨(dú)介紹。
2.2 類加載雙親委派機(jī)制介紹和分析
?????? 在這里,需要著重說明的是, JVM在加載類時默認(rèn)采用的是雙親委派 機(jī)制。通俗的講,就是某個特定的類加載器在接到加載類的請求時,首先將加載任務(wù)委托給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務(wù),就成功返回;只有父類加載器無法完成此加載任務(wù)時,才自己去加載。 關(guān)于虛擬機(jī)默認(rèn)的雙親委派機(jī)制,我們可以從系統(tǒng)類加載器和擴(kuò)展類加載器為例作簡單分析。
圖一 標(biāo)準(zhǔn)擴(kuò)展類加載器繼承層次圖
圖二 系統(tǒng)類加載器繼承層次圖
通過圖一和圖二我們可以看出,類加載器均是繼承自 java.lang.ClassLoader抽象類。我們下面我們就看簡要介紹一下 java.lang.ClassLoader中幾個最重要的方法 :
[java] view plaincopyprint?
? public Class<?> loadClass(String name) throws ClassNotFoundException{ … }? ? ? protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ … }? ? ? protected Class<?> findClass(String name) throws ClassNotFoundException { … }? ? ? ? protected final Class<?> defineClass(String name, byte [] b, int off, int len) throws ClassFormatError{ … }?
//加載指定名稱(包括包名)的二進(jìn)制類型,供用戶調(diào)用的接口
public Class<?> loadClass(String name) throws ClassNotFoundException{ … }//加載指定名稱(包括包名)的二進(jìn)制類型,同時指定是否解析(但是這里的resolve參數(shù)不一定真正能達(dá)到解析的效果),供繼承用
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ … }//findClass方法一般被loadClass方法調(diào)用去加載指定名稱類,供繼承用
protected Class<?> findClass(String name) throws ClassNotFoundException { … }//定義類型,一般在findClass方法中讀取到對應(yīng)字節(jié)碼后調(diào)用,可以看出不可繼承
//(說明:JVM已經(jīng)實(shí)現(xiàn)了對應(yīng)的具體功能,解析對應(yīng)的字節(jié)碼,產(chǎn)生對應(yīng)的內(nèi)部數(shù)據(jù)結(jié)構(gòu)放置到方法區(qū),所以無需覆寫,直接調(diào)用就可以了)
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ … }
通過進(jìn)一步分析標(biāo)準(zhǔn)擴(kuò)展類加載器( sun.misc.Launcher$ExtClassLoader )和系統(tǒng)類加載器( sun.misc.Launcher$AppClassLoader )的代碼以及其公共父類( java.net.URLClassLoader和 java.security.SecureClassLoader)的代碼可以看出,都沒有覆寫 java.lang.ClassLoader中默認(rèn)的加載委派規(guī)則 ---loadClass( … )方法。 既然這樣,我們就可以通過分析 java.lang.ClassLoader中的 loadClass( String name)方法的代碼就可以分析出虛擬機(jī)默認(rèn)采用的雙親委派機(jī)制到底是什么模樣:
[java] view plaincopyprint?
public Class<?> loadClass(String name) throws ClassNotFoundException {? ??? return loadClass(name, false );? }? ? protected synchronized Class<?> loadClass(String name, boolean resolve)? ??????? throws ClassNotFoundException {? ? ??? ? ??? Class c = findLoadedClass(name);? ??? if (c == null ) {? ??????? ? ??????? try {? ??????????? if (parent != null ) {? ??????????????? ? ??????????????? c = parent.loadClass(name, false );? ??????????? } else {? ??????????????? ? ??????????????? ? ??????????????? c = findBootstrapClass0(name);? ??????????? }? ??????? } catch (ClassNotFoundException e) {? ??????????? ? ??????????? c = findClass(name);? ??????? }? ??? }? ??? if (resolve) {? ??????? resolveClass(c);? ??? }? ??? return c;? }?
public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);}protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {// 首先判斷該類型是否已經(jīng)被加載Class c = findLoadedClass(name);if (c == null) {//如果沒有被加載,就委托給父類加載或者委派給啟動類加載器加載try {if (parent != null) {//如果存在父類加載器,就委派給父類加載器加載c = parent.loadClass(name, false);} else {//如果不存在父類加載器,就檢查是否是由啟動類加載器加載的類,//通過調(diào)用本地方法native findBootstrapClass0(String name)c = findBootstrapClass0(name);}} catch (ClassNotFoundException e) {// 如果父類加載器和啟動類加載器都不能完成加載任務(wù),才調(diào)用自身的加載功能c = findClass(name);}}if (resolve) {resolveClass(c);}return c;}
通過上面的代碼分析,我們可以對 JVM采用的雙親委派類加載機(jī)制有了更感性的認(rèn)識,下面我們就接著分析一下啟動類加載器、標(biāo)準(zhǔn)擴(kuò)展類加載器和系統(tǒng)類加載器三者之間的關(guān)系。可能大家已經(jīng)從各種資料上面看到了如下類似的一幅圖片:
圖三 類加載器默認(rèn)委派關(guān)系圖
上面圖片給人的直觀印象是系統(tǒng)類加載器的父類加載器是標(biāo)準(zhǔn)擴(kuò)展類加載器,標(biāo)準(zhǔn)擴(kuò)展類加載器的父類加載器是啟動類加載器,下面我們就用代碼具體測試一下:
[java] view plaincopyprint?
public class LoaderTest {? ? ??? public static void main(String[] args) {? ??????? try {? ??????????? System.out.println(ClassLoader.getSystemClassLoader());? ??????????? System.out.println(ClassLoader.getSystemClassLoader().getParent());? ??????????? System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());? ??????? } catch (Exception e) {? ??????????? e.printStackTrace();? ??????? }? ??? }? }?
public class LoaderTest {public static void main(String[] args) {try {System.out.println(ClassLoader.getSystemClassLoader());System.out.println(ClassLoader.getSystemClassLoader().getParent());System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());} catch (Exception e) {e.printStackTrace();}}
}
說明:通過java.lang.ClassLoader.getSystemClassLoader()可以直接獲取到系統(tǒng)類加載器。 代碼輸出如下:
[plain] view plaincopyprint?
sun.misc.Launcher$AppClassLoader@6d06d69c? sun.misc.Launcher$ExtClassLoader@70dea4e? null?
sun.misc.Launcher$AppClassLoader@6d06d69c
sun.misc.Launcher$ExtClassLoader@70dea4e
null
通過以上的代碼輸出,我們可以判定系統(tǒng)類加載器的父加載器是標(biāo)準(zhǔn)擴(kuò)展類加載器,但是我們試圖獲取標(biāo)準(zhǔn)擴(kuò)展類加載器的父類加載器時確得到了 null ,就是說標(biāo)準(zhǔn)擴(kuò)展類加載器本身強(qiáng)制設(shè)定父類加載器為 null 。 我們還是借助于代碼分析一下。
我們首先看一下 java.lang.ClassLoader 抽象類中默認(rèn)實(shí)現(xiàn)的兩個構(gòu)造函數(shù):
[java] view plaincopyprint?
protected ClassLoader() {? ??? SecurityManager security = System.getSecurityManager();? ??? if (security != null ) {? ??????? security.checkCreateClassLoader();? ??? }? ??? ? ??? this .parent = getSystemClassLoader();? ??? initialized = true ;? }? ? protected ClassLoader(ClassLoader parent) {? ??? SecurityManager security = System.getSecurityManager();? ??? if (security != null ) {? ??????? security.checkCreateClassLoader();? ??? }? ??? ? ??? this .parent = parent;? ??? initialized = true ;? }?
protected ClassLoader() {SecurityManager security = System.getSecurityManager();if (security != null) {security.checkCreateClassLoader();}//默認(rèn)將父類加載器設(shè)置為系統(tǒng)類加載器,getSystemClassLoader()獲取系統(tǒng)類加載器this.parent = getSystemClassLoader();initialized = true;}protected ClassLoader(ClassLoader parent) {SecurityManager security = System.getSecurityManager();if (security != null) {security.checkCreateClassLoader();}//強(qiáng)制設(shè)置父類加載器this.parent = parent;initialized = true;}
我們再看一下 ClassLoader 抽象類中 parent 成員的聲明:
[java] view plaincopyprint?
? private ClassLoader parent;?
// The parent class loader for delegation
private ClassLoader parent;
聲明為私有變量的同時并沒有對外提供可供派生類訪問的public或者protected設(shè)置器接口(對應(yīng)的setter方法),結(jié)合前面的測試代碼的輸出,我們可以推斷出: 1. 系統(tǒng)類加載器(AppClassLoader)調(diào)用ClassLoader(ClassLoader parent)構(gòu)造函數(shù)將父類加載器設(shè)置為標(biāo)準(zhǔn)擴(kuò)展類加載器(ExtClassLoader)。(因?yàn)槿绻粡?qiáng)制設(shè)置,默認(rèn)會通過調(diào)用getSystemClassLoader()方法獲取并設(shè)置成系統(tǒng)類加載器,這顯然和測試輸出結(jié)果不符。) 2. 擴(kuò)展類加載器(ExtClassLoader)調(diào)用ClassLoader(ClassLoader parent)構(gòu)造函數(shù)將父類加載器設(shè)置為null。(因?yàn)槿绻粡?qiáng)制設(shè)置,默認(rèn)會通過調(diào)用getSystemClassLoader()方法獲取并設(shè)置成系統(tǒng)類加載器,這顯然和測試輸出結(jié)果不符。) 現(xiàn)在我們可能會有這樣的疑問:擴(kuò)展類加載器(ExtClassLoader)的父類加載器被強(qiáng)制設(shè)置為null了,那么擴(kuò)展類加載器為什么還能將加載任務(wù)委派給啟動類加載器呢?
圖四 標(biāo)準(zhǔn)擴(kuò)展類加載器和系統(tǒng)類加載器成員大綱視圖
圖五 擴(kuò)展類加載器和系統(tǒng)類加載器公共父類成員大綱視圖
通過圖四和圖五可以看出,標(biāo)準(zhǔn)擴(kuò)展類加載器和系統(tǒng)類加載器及其父類(java.net.URLClassLoader和java.security.SecureClassLoader)都沒有覆寫java.lang.ClassLoader中默認(rèn)的加載委派規(guī)則---loadClass(…)方法。 有關(guān)java.lang.ClassLoader中默認(rèn)的加載委派規(guī)則前面已經(jīng)分析過,如果父加載器為null,則會調(diào)用本地方法進(jìn)行啟動類加載嘗試。所以,圖三中,啟動類加載器、標(biāo)準(zhǔn)擴(kuò)展類加載器和系統(tǒng)類加載器之間的委派關(guān)系事實(shí)上是仍就成立的。(在后面的用戶自定義類加載器部分,還會做更深入的分析)。
2.3 類加載雙親委派示例
以上已經(jīng)簡要介紹了虛擬機(jī)默認(rèn)使用的啟動類加載器、標(biāo)準(zhǔn)擴(kuò)展類加載器和系統(tǒng)類加載器,并以三者為例結(jié)合JDK代碼對JVM默認(rèn)使用的雙親委派類加載機(jī)制做了分析。下面我們就來看一個綜合的例子。首先在IDE中建立一個簡單的java應(yīng)用工程,然后寫一個簡單的JavaBean如下:
[java] view plaincopyprint?
package classloader.test.bean;? ? public class TestBean {? ????? ??? public TestBean() { }? }?
package classloader.test.bean;public class TestBean {public TestBean() { }
}
在現(xiàn)有當(dāng)前工程中另外建立一測試類(ClassLoaderTest.java)內(nèi)容如下:
測試一:
[java] view plaincopyprint?
package classloader.test.bean;? ? public class ClassLoaderTest {? ? ??? public static void main(String[] args) {? ??????? try {? ??????????? ? ??????????? System.out.println(System.getProperty("java.class.path" ));? ??????????? ? ??????????? Class typeLoaded = Class.forName("classloader.test.bean.TestBean" );? ??????????? ? ??????????? System.out.println(typeLoaded.getClassLoader());? ??????? } catch (Exception e) {? ??????????? e.printStackTrace();? ??????? }? ??? }? }?
package classloader.test.bean;public class ClassLoaderTest {public static void main(String[] args) {try {//查看當(dāng)前系統(tǒng)類路徑中包含的路徑條目System.out.println(System.getProperty("java.class.path"));//調(diào)用加載當(dāng)前類的類加載器(這里即為系統(tǒng)類加載器)加載TestBeanClass typeLoaded = Class.forName("classloader.test.bean.TestBean");//查看被加載的TestBean類型是被那個類加載器加載的System.out.println(typeLoaded.getClassLoader());} catch (Exception e) {e.printStackTrace();}}
}
對應(yīng)的輸出如下:
[plain] view plaincopyprint?
C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes? sun.misc.Launcher$AppClassLoader@73d16e93?
C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes
sun.misc.Launcher$AppClassLoader@73d16e93
說明:當(dāng)前類路徑默認(rèn)的含有的一個條目就是工程的輸出目錄。 測試二:
將當(dāng)前工程輸出目錄下的 TestBean.class打包進(jìn) test.jar 剪貼 到 <Java_Runtime_Home>/lib/ext 目錄下(現(xiàn)在工程輸出目錄下和 JRE 擴(kuò)展目錄下都有待加載類型的 class 文件)。再運(yùn)行測試一測試代碼,結(jié)果如下:
[plain] view plaincopyprint?
C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes? sun.misc.Launcher$ExtClassLoader@15db9742?
C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes
sun.misc.Launcher$ExtClassLoader@15db9742
對比測試一和測試二,我們明顯可以驗(yàn)證前面說的雙親委派機(jī)制,系統(tǒng)類加載器在接到加載classloader.test.bean.TestBean類型的請求時,首先將請求委派給父類加載器(標(biāo)準(zhǔn)擴(kuò)展類加載器),標(biāo)準(zhǔn)擴(kuò)展類加載器搶先完成了加載請求。 測試三: 將test.jar拷貝一份到<Java_Runtime_Home>/lib下,運(yùn)行測試代碼,輸出如下:
[plain] view plaincopyprint?
C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes? sun.misc.Launcher$ExtClassLoader@15db9742?
C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes
sun.misc.Launcher$ExtClassLoader@15db9742
測試三和測試二輸出結(jié)果一致。那就是說,放置到<Java_Runtime_Home>/lib目錄下的TestBean對應(yīng)的class字節(jié)碼并沒有被加載,這其實(shí)和前面講的雙親委派機(jī)制并不矛盾。虛擬機(jī)出于安全等因素考慮,不會加載<Java_Runtime_Home>/lib存在的陌生類,開發(fā)者通過將要加載的非JDK自身的類放置到此目錄下期待啟動類加載器加載是不可能的。 做個進(jìn)一步驗(yàn)證,刪除<Java_Runtime_Home>/lib/ext目錄下和工程輸出目錄下的TestBean對應(yīng)的class文件,然后再運(yùn)行測試代碼,則將會有ClassNotFoundException異常拋出。有關(guān)這個問題,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中設(shè)置相應(yīng)斷點(diǎn)運(yùn)行測試三進(jìn)行調(diào)試,會發(fā)現(xiàn)findBootstrapClass0()會拋出異常,然后在下面的findClass方法中被加載,當(dāng)前運(yùn)行的類加載器正是擴(kuò)展類加載器(sun.misc.Launcher$ExtClassLoader),這一點(diǎn)可以通過JDT中變量視圖查看驗(yàn)證。
3 java程序動態(tài)擴(kuò)展方式
Java的連接模型允許用戶運(yùn)行時擴(kuò)展引用程序,既可以通過當(dāng)前虛擬機(jī)中預(yù)定義的加載器加載編譯時已知的類或者接口,又允許用戶自行定義類裝載器,在運(yùn)行時動態(tài)擴(kuò)展用戶的程序。通過用戶自定義的類裝載器,你的程序可以裝載在編譯時并不知道或者尚未存在的類或者接口,并動態(tài)連接它們并進(jìn)行有選擇的解析。 運(yùn)行時動態(tài)擴(kuò)展java應(yīng)用程序有如下兩個途徑:
3.1 調(diào)用java.lang.Class.forName(…)加載類
這個方法其實(shí)在前面已經(jīng)討論過,在后面的問題2解答中說明了該方法調(diào)用會觸發(fā)哪個類加載器開始加載任務(wù)。這里需要說明的是多參數(shù)版本的forName(…)方法:
[java] view plaincopyprint?
public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException?
public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException
這里的initialize參數(shù)是很重要的。它表示在加載同時是否完成初始化的工作(說明:單參數(shù)版本的forName方法默認(rèn)是完成初始化的)。有些場景下需要將initialize設(shè)置為true來強(qiáng)制加載同時完成初始化。例如典型的就是利用DriverManager進(jìn)行JDBC驅(qū)動程序類注冊的問題。 因?yàn)槊恳粋€JDBC驅(qū)動程序類的靜態(tài)初始化方法都用DriverManager注冊驅(qū)動程序,這樣才能被應(yīng)用程序使用。這就要求驅(qū)動程序類必須被初始化,而不單單被加載。Class.forName的一個很常見的用法就是在加載數(shù)據(jù)庫驅(qū)動的時候。如 Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()用來加載 Apache Derby 數(shù)據(jù)庫的驅(qū)動。
3.2 用戶自定義類加載器
通過前面的分析,我們可以看出,除了和本地實(shí)現(xiàn)密切相關(guān)的啟動類加載器之外,包括標(biāo)準(zhǔn)擴(kuò)展類加載器和系統(tǒng)類加載器在內(nèi)的所有其他類加載器我們都可以當(dāng)做自定義類加載器來對待,唯一區(qū)別是是否被虛擬機(jī)默認(rèn)使用。前面的內(nèi)容中已經(jīng)對java.lang.ClassLoader抽象類中的幾個重要的方法做了介紹,這里就簡要敘述一下一般用戶自定義類加載器的工作流程吧(可以結(jié)合后面問題解答一起看): 1、首先檢查請求的類型是否已經(jīng)被這個類裝載器裝載到命名空間中了,如果已經(jīng)裝載,直接返回;否則轉(zhuǎn)入步驟2; 2、委派類加載請求給父類加載器(更準(zhǔn)確的說應(yīng)該是雙親類加載器,真實(shí)虛擬機(jī)中各種類加載器最終會呈現(xiàn)樹狀結(jié)構(gòu)),如果父類加載器能夠完成,則返回父類加載器加載的Class實(shí)例;否則轉(zhuǎn)入步驟3; 3、調(diào)用本類加載器的findClass(…)方法,試圖獲取對應(yīng)的字節(jié)碼,如果獲取的到,則調(diào)用defineClass(…)導(dǎo)入類型到方法區(qū);如果獲取不到對應(yīng)的字節(jié)碼或者其他原因失敗,返回異常給loadClass(…), loadClass(…)轉(zhuǎn)而拋異常,終止加載過程(注意:這里的異常種類不止一種)。 說明:這里說的自定義類加載器是指JDK 1.2以后版本的寫法,即不覆寫改變java.lang.loadClass(…)已有委派邏輯情況下。
整個加載類的過程如下圖:
圖六 自定義類加載器加載類的過程
4 常見問題分析
4.1 由不同的類加載器加載的指定類還是相同的類型嗎?
在Java中,一個類用其完全匹配類名(fully qualified class name)作為標(biāo)識,這里指的完全匹配類名包括包名和類名。但在JVM中一個類用其全名和一個加載類ClassLoader的實(shí)例作為唯一標(biāo)識,不同類加載器加載的類將被置于不同的命名空間。 我們可以用兩個自定義類加載器去加載某自定義類型(注意不要將自定義類型的字節(jié)碼放置到系統(tǒng)路徑或者擴(kuò)展路徑中,否則會被系統(tǒng)類加載器或擴(kuò)展類加載器搶先加載),然后用獲取到的兩個Class實(shí)例進(jìn)行java.lang.Object.equals(…)判斷,將會得到不相等的結(jié)果。這個大家可以寫兩個自定義的類加載器去加載相同的自定義類型,然后做個判斷;同時,可以測試加載java.*類型,然后再對比測試一下測試結(jié)果。
4.2 在代碼中直接調(diào)用Class.forName(String name)方法,到底會觸發(fā)那個類加載器進(jìn)行類加載行為?
Class.forName(String name)默認(rèn)會使用調(diào)用類的類加載器來進(jìn)行類加載。我們直接來分析一下對應(yīng)的jdk的代碼:
[java] view plaincopyprint?
? publicstatic Class<?> forName(String className) throws ClassNotFoundException {? ??? return forName0(className, true , ClassLoader.getCallerClassLoader());? }? ? ? ? static ClassLoader getCallerClassLoader() {? ??? ? ??? Class caller = Reflection.getCallerClass(3 );? ??? ? ??? if (caller == null ) {? ??????? return null ;? ??? }? ??? ? ??? return caller.getClassLoader0();? }? ? ? ? native ClassLoader getClassLoader0();?
//java.lang.Class.javapublicstatic Class<?> forName(String className) throws ClassNotFoundException {return forName0(className, true, ClassLoader.getCallerClassLoader());}//java.lang.ClassLoader.java// Returns the invoker's class loader, or null if none.static ClassLoader getCallerClassLoader() {// 獲取調(diào)用類(caller)的類型Class caller = Reflection.getCallerClass(3);// This can be null if the VM is requesting itif (caller == null) {return null;}// 調(diào)用java.lang.Class中本地方法獲取加載該調(diào)用類(caller)的ClassLoaderreturn caller.getClassLoader0();}//java.lang.Class.java//虛擬機(jī)本地實(shí)現(xiàn),獲取當(dāng)前類的類加載器,前面介紹的Class的getClassLoader()也使用此方法native ClassLoader getClassLoader0();
4.3 在編寫自定義類加載器時,如果沒有設(shè)定父加載器,那么父加載器是誰?
前面講過,在不指定父類加載器的情況下,默認(rèn)采用系統(tǒng)類加載器。 可能有人覺得不明白,現(xiàn)在我們來看一下JDK對應(yīng)的代碼實(shí)現(xiàn)。眾所周知,我們編寫自定義的類加載器直接或者間接繼承自java.lang.ClassLoader抽象類,對應(yīng)的無參默認(rèn)構(gòu)造函數(shù)實(shí)現(xiàn)如下:
[java] view plaincopyprint?
? protected ClassLoader() {? ??? SecurityManager security = System.getSecurityManager();? ??? if (security != null ) {? ??????? security.checkCreateClassLoader();? ??? }? ??? this .parent = getSystemClassLoader();? ??? initialized = true ;? }?
//摘自java.lang.ClassLoader.javaprotected ClassLoader() {SecurityManager security = System.getSecurityManager();if (security != null) {security.checkCreateClassLoader();}this.parent = getSystemClassLoader();initialized = true;}
我們再來看一下對應(yīng)的getSystemClassLoader()方法的實(shí)現(xiàn):
[java] view plaincopyprint?
private static synchronized void initSystemClassLoader() {? ??? ? ??? sun.misc.Launcher l = sun.misc.Launcher.getLauncher();? ??? scl = l.getClassLoader();? ??? ? }?
private static synchronized void initSystemClassLoader() {//...sun.misc.Launcher l = sun.misc.Launcher.getLauncher();scl = l.getClassLoader();//...}
我們可以寫簡單的測試代碼來測試一下:
[java] view plaincopyprint?
System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());?
System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());
本機(jī)對應(yīng)輸出如下:
[plain] view plaincopyprint?
sun.misc.Launcher$AppClassLoader@73d16e93?
sun.misc.Launcher$AppClassLoader@73d16e93
所以,我們現(xiàn)在可以相信當(dāng)自定義類加載器沒有指定父類加載器的情況下,默認(rèn)的父類加載器即為系統(tǒng)類加載器。同時,我們可以得出如下結(jié)論:即使用戶自定義類加載器不指定父類加載器,那么,同樣可以加載如下三個地方的類: 1. <Java_Runtime_Home>/lib下的類; 2. < Java_Runtime_Home >/lib/ext下或者由系統(tǒng)變量java.ext.dir指定位置中的類; 3. 當(dāng)前工程類路徑下或者由系統(tǒng)變量java.class.path指定位置中的類。
4.4 在編寫自定義類加載器時,如果將父類加載器強(qiáng)制設(shè)置為null,那么會有什么影響?如果自定義的類加載器不能加載指定類,就肯定會加載失敗嗎?
JVM規(guī)范中規(guī)定如果用戶自定義的類加載器將父類加載器強(qiáng)制設(shè)置為null,那么會自動將啟動類加載器設(shè)置為當(dāng)前用戶自定義類加載器的父類加載器(這個問題前面已經(jīng)分析過了)。同時,我們可以得出如下結(jié)論: 即使用戶自定義類加載器不指定父類加載器,那么,同樣可以加載到<Java_Runtime_Home>/lib下的類,但此時就不能夠加載<Java_Runtime_Home>/lib/ext目錄下的類了。 說明:問題3和問題4的推斷結(jié)論是基于用戶自定義的類加載器本身延續(xù)了java.lang.ClassLoader.loadClass(…)默認(rèn)委派邏輯,如果用戶對這一默認(rèn)委派邏輯進(jìn)行了改變,以上推斷結(jié)論就不一定成立了,詳見問題5。
4.5 編寫自定義類加載器時,一般有哪些注意點(diǎn)?
1、一般盡量不要覆寫已有的loadClass(...)方法中的委派邏輯 一般在JDK 1.2之前的版本才這樣做,而且事實(shí)證明,這樣做極有可能引起系統(tǒng)默認(rèn)的類加載器不能正常工作。在JVM規(guī)范和JDK文檔中(1.2或者以后版本中),都沒有建議用戶覆寫loadClass(…)方法,相比而言,明確提示開發(fā)者在開發(fā)自定義的類加載器時覆寫findClass(…)邏輯。舉一個例子來驗(yàn)證該問題:
[java] view plaincopyprint?
? public class WrongClassLoader extends ClassLoader {? ? ??? public Class<?> loadClass(String name) throws ClassNotFoundException {? ??????? return this .findClass(name);? ??? }? ? ??? protected Class<?> findClass(String name) throws ClassNotFoundException {? ??????? ? ??????? ? ??? }? }?
//用戶自定義類加載器WrongClassLoader.Java(覆寫loadClass邏輯)
public class WrongClassLoader extends ClassLoader {public Class<?> loadClass(String name) throws ClassNotFoundException {return this.findClass(name);}protected Class<?> findClass(String name) throws ClassNotFoundException {// 假設(shè)此處只是到工程以外的特定目錄D:\library下去加載類// 具體實(shí)現(xiàn)代碼省略}
}
通過前面的分析我們已經(jīng)知道,這個自定義類加載器WrongClassLoader的默認(rèn)類加載器是系統(tǒng)類加載器,但是現(xiàn)在問題4種的結(jié)論就不成立了。大家可以簡單測試一下,現(xiàn)在<Java_Runtime_Home>/lib、< Java_Runtime_Home >/lib/ext和工程類路徑上的類都加載不上了。
[java] view plaincopyprint?
? public class WrongClassLoaderTest {? ? ??? publicstaticvoid main(String[] args) {? ??????? try {? ??????????? WrongClassLoader loader = new WrongClassLoader();? ??????????? Class classLoaded = loader.loadClass("beans.Account" );? ??????????? System.out.println(classLoaded.getName());? ??????????? System.out.println(classLoaded.getClassLoader());? ??????? } catch (Exception e) {? ??????????? e.printStackTrace();? ??????? }? ??? }? }?
//問題5測試代碼一
public class WrongClassLoaderTest {publicstaticvoid main(String[] args) {try {WrongClassLoader loader = new WrongClassLoader();Class classLoaded = loader.loadClass("beans.Account");System.out.println(classLoaded.getName());System.out.println(classLoaded.getClassLoader());} catch (Exception e) {e.printStackTrace();}}
}
這里D:"classes"beans"Account.class是物理存在的。輸出結(jié)果:
[plain] view plaincopyprint?
java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系統(tǒng)找不到指定的路徑。)? ??? at java.io.FileInputStream.open(Native Method)? ??? at java.io.FileInputStream.<init>(FileInputStream.java:106)? ??? at WrongClassLoader.findClass(WrongClassLoader.java:40)? ??? at WrongClassLoader.loadClass(WrongClassLoader.java:29)? ??? at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)? ??? at java.lang.ClassLoader.defineClass1(Native Method)? ??? at java.lang.ClassLoader.defineClass(ClassLoader.java:620)? ??? at java.lang.ClassLoader.defineClass(ClassLoader.java:400)? ??? at WrongClassLoader.findClass(WrongClassLoader.java:43)? ??? at WrongClassLoader.loadClass(WrongClassLoader.java:29)? ??? at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)? Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object? ??? at java.lang.ClassLoader.defineClass1(Native Method)? ??? at java.lang.ClassLoader.defineClass(ClassLoader.java:620)? ??? at java.lang.ClassLoader.defineClass(ClassLoader.java:400)? ??? at WrongClassLoader.findClass(WrongClassLoader.java:43)? ??? at WrongClassLoader.loadClass(WrongClassLoader.java:29)? ??? at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)?
java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系統(tǒng)找不到指定的路徑。)at java.io.FileInputStream.open(Native Method)at java.io.FileInputStream.<init>(FileInputStream.java:106)at WrongClassLoader.findClass(WrongClassLoader.java:40)at WrongClassLoader.loadClass(WrongClassLoader.java:29)at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)at java.lang.ClassLoader.defineClass1(Native Method)at java.lang.ClassLoader.defineClass(ClassLoader.java:620)at java.lang.ClassLoader.defineClass(ClassLoader.java:400)at WrongClassLoader.findClass(WrongClassLoader.java:43)at WrongClassLoader.loadClass(WrongClassLoader.java:29)at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Objectat java.lang.ClassLoader.defineClass1(Native Method)at java.lang.ClassLoader.defineClass(ClassLoader.java:620)at java.lang.ClassLoader.defineClass(ClassLoader.java:400)at WrongClassLoader.findClass(WrongClassLoader.java:43)at WrongClassLoader.loadClass(WrongClassLoader.java:29)at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)
這說明,連要加載的類型的超類型java.lang.Object都加載不到了。這里列舉的由于覆寫loadClass()引起的邏輯錯誤明顯是比較簡單的,實(shí)際引起的邏輯錯誤可能復(fù)雜的多。
[java] view plaincopyprint?
? ? public class WrongClassLoader extends ClassLoader {? ? ??? protected Class<?> findClass(String name) throws ClassNotFoundException {? ??????? ? ??????? ? ??? }? }?
//問題5測試二
//用戶自定義類加載器WrongClassLoader.Java(不覆寫loadClass邏輯)
public class WrongClassLoader extends ClassLoader {protected Class<?> findClass(String name) throws ClassNotFoundException {//假設(shè)此處只是到工程以外的特定目錄D:\library下去加載類//具體實(shí)現(xiàn)代碼省略}
}
將自定義類加載器代碼WrongClassLoader.Java做以上修改后,再運(yùn)行測試代碼,輸出結(jié)果如下:
[plain] view plaincopyprint?
beans.Account? WrongClassLoader@1c78e57?
beans.Account
WrongClassLoader@1c78e57
2、正確設(shè)置父類加載器 通過上面問題4和問題5的分析我們應(yīng)該已經(jīng)理解,個人覺得這是自定義用戶類加載器時最重要的一點(diǎn),但常常被忽略或者輕易帶過。有了前面JDK代碼的分析作為基礎(chǔ),我想現(xiàn)在大家都可以隨便舉出例子了。 3、保證findClass(String name)方法的邏輯正確性 事先盡量準(zhǔn)確理解待定義的類加載器要完成的加載任務(wù),確保最大程度上能夠獲取到對應(yīng)的字節(jié)碼內(nèi)容。
4.6 如何在運(yùn)行時判斷系統(tǒng)類加載器能加載哪些路徑下的類?
一是可以直接調(diào)用ClassLoader.getSystemClassLoader()或者其他方式獲取到系統(tǒng)類加載器(系統(tǒng)類加載器和擴(kuò)展類加載器本身都派生自URLClassLoader),調(diào)用URLClassLoader中的getURLs()方法可以獲取到。 二是可以直接通過獲取系統(tǒng)屬性java.class.path來查看當(dāng)前類路徑上的條目信息 :System.getProperty("java.class.path")。
4.7 如何在運(yùn)行時判斷標(biāo)準(zhǔn)擴(kuò)展類加載器能加載哪些路徑下的類?
方法之一:
[java] view plaincopyprint?
import java.net.URL;? import java.net.URLClassLoader;? ? public class ClassLoaderTest {? ? ??? ? ??? public static void main(String[] args) {? ??????? try {? ??????????? URL[] extURLs = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();? ??????????? for (int i = 0 ; i < extURLs.length; i++) {? ??????????????? System.out.println(extURLs[i]);? ??????????? }? ??????? } catch (Exception e) {? ??????????? ? ??????? }? ??? }? }?
import java.net.URL;
import java.net.URLClassLoader;public class ClassLoaderTest {/*** @param args the command line arguments*/public static void main(String[] args) {try {URL[] extURLs = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();for (int i = 0; i < extURLs.length; i++) {System.out.println(extURLs[i]);}} catch (Exception e) {//…}}
}
本機(jī)對應(yīng)輸出如下:
[plain] view plaincopyprint?
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/access-bridge-64.jar? file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/cldrdata.jar? file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/dnsns.jar? file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/jaccess.jar? file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/jfxrt.jar? file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/localedata.jar? file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/nashorn.jar? file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunec.jar? file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunjce_provider.jar? file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunmscapi.jar? file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunpkcs11.jar? file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/zipfs.jar?
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/access-bridge-64.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/cldrdata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/dnsns.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/jaccess.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/jfxrt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/localedata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/nashorn.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunec.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunjce_provider.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunmscapi.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunpkcs11.jar
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/zipfs.jar
5 開發(fā)自己的類加載器
在前面介紹類加載器的代理委派模式的時候,提到過類加載器會首先代理給其它類加載器來嘗試加載某個類。這就意味著真正完成類的加載工作的類加載器和啟動這個加載過程的類加載器,有可能不是同一個。真正完成類的加載工作是通過調(diào)用defineClass來實(shí)現(xiàn)的;而啟動類的加載過程是通過調(diào)用loadClass來實(shí)現(xiàn)的。前者稱為一個類的定義加載器(defining loader),后者稱為初始加載器(initiating loader)。在Java虛擬機(jī)判斷兩個類是否相同的時候,使用的是類的定義加載器。也就是說,哪個類加載器啟動類的加載過程并不重要,重要的是最終定義這個類的加載器。兩種類加載器的關(guān)聯(lián)之處在于:一個類的定義加載器是它引用的其它類的初始加載器。 如類 com.example.Outer引用了類 com.example.Inner,則由類 com.example.Outer的定義加載器負(fù)責(zé)啟動類 com.example.Inner的加載過程。 方法 loadClass()拋出的是 java.lang.ClassNotFoundException異常;方法 defineClass()拋出的是 java.lang.NoClassDefFoundError異常。 類加載器在成功加載某個類之后,會把得到的 java.lang.Class類的實(shí)例緩存起來。下次再請求加載該類的時候,類加載器會直接使用緩存的類的實(shí)例,而不會嘗試再次加載。也就是說,對于一個類加載器實(shí)例來說,相同全名的類只加載一次,即 loadClass方法不會被重復(fù)調(diào)用。
在絕大多數(shù)情況下,系統(tǒng)默認(rèn)提供的類加載器實(shí)現(xiàn)已經(jīng)可以滿足需求。但是在某些情況下,您還是需要為應(yīng)用開發(fā)出自己的類加載器。比如您的應(yīng)用通過網(wǎng)絡(luò)來傳輸Java類的字節(jié)代碼,為了保證安全性,這些字節(jié)代碼經(jīng)過了加密處理。這個時候您就需要自己的類加載器來從某個網(wǎng)絡(luò)地址上讀取加密后的字節(jié)代碼,接著進(jìn)行解密和驗(yàn)證,最后定義出要在Java虛擬機(jī)中運(yùn)行的類來。下面將通過兩個具體的實(shí)例來說明類加載器的開發(fā)。
5.1 文件系統(tǒng)類加載器
第一個類加載器用來加載存儲在文件系統(tǒng)上的Java字節(jié)代碼。完整的實(shí)現(xiàn)如下所示。
[java] view plaincopyprint?
package classloader;? ? import java.io.ByteArrayOutputStream;? import java.io.File;? import java.io.FileInputStream;? import java.io.IOException;? import java.io.InputStream;? ? ? public class FileSystemClassLoader extends ClassLoader {? ? ??? private String rootDir;? ? ??? public FileSystemClassLoader(String rootDir) {? ??????? this .rootDir = rootDir;? ??? }? ? ??? ? ??? @Override ? ??? 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 (IOException e) {? ??????????? e.printStackTrace();? ??????? }? ??????? return null ;? ??? }? ? ??? private String classNameToPath(String className) {? ??????? ? ??????? return rootDir + File.separatorChar? ??????????????? + className.replace('.' , File.separatorChar) + ".class" ;? ??? }? }?
package classloader;import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;// 文件系統(tǒng)類加載器
public class FileSystemClassLoader extends ClassLoader {private String rootDir;public FileSystemClassLoader(String rootDir) {this.rootDir = rootDir;}// 獲取類的字節(jié)碼@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {byte[] classData = getClassData(name); // 獲取類的字節(jié)數(shù)組if (classData == null) {throw new ClassNotFoundException();} else {return defineClass(name, classData, 0, classData.length);}}private byte[] getClassData(String className) {// 讀取類文件的字節(jié)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;// 讀取類文件的字節(jié)碼while ((bytesNumRead = ins.read(buffer)) != -1) {baos.write(buffer, 0, bytesNumRead);}return baos.toByteArray();} catch (IOException e) {e.printStackTrace();}return null;}private String classNameToPath(String className) {// 得到類文件的完全路徑return rootDir + File.separatorChar+ className.replace('.', File.separatorChar) + ".class";}
}
如上所示,類 FileSystemClassLoader繼承自類java.lang.ClassLoader。在java.lang.ClassLoader類的常用方法中,一般來說,自己開發(fā)的類加載器只需要覆寫 findClass(String name)方法即可。java.lang.ClassLoader類的方法loadClass()封裝了前面提到的代理模式的實(shí)現(xiàn)。該方法會首先調(diào)用findLoadedClass()方法來檢查該類是否已經(jīng)被加載過;如果沒有加載過的話,會調(diào)用父類加載器的loadClass()方法來嘗試加載該類;如果父類加載器無法加載該類的話,就調(diào)用findClass()方法來查找該類。因此,為了保證類加載器都正確實(shí)現(xiàn)代理模式,在開發(fā)自己的類加載器時,最好不要覆寫 loadClass()方法,而是覆寫 findClass()方法。 類 FileSystemClassLoader的 findClass()方法首先根據(jù)類的全名在硬盤上查找類的字節(jié)代碼文件(.class 文件),然后讀取該文件內(nèi)容,最后通過defineClass()方法來把這些字節(jié)代碼轉(zhuǎn)換成 java.lang.Class類的實(shí)例。
加載本地文件系統(tǒng)上的類,示例如下:
[java] view plaincopyprint?
package com.example;? ? public class Sample {? ? ??? private Sample instance;? ? ??? public void setSample(Object instance) {? ??????? System.out.println(instance.toString());? ??????? this .instance = (Sample) instance;? ??? }? }?
package com.example;public class Sample {private Sample instance;public void setSample(Object instance) {System.out.println(instance.toString());this.instance = (Sample) instance;}
}
[java] view plaincopyprint?
package classloader;? ? import java.lang.reflect.Method;? ? public class ClassIdentity {? ? ??? public static void main(String[] args) {? ??????? new ClassIdentity().testClassIdentity();? ??? }? ? ??? public void testClassIdentity() {? ??????? String classDataRootPath = "C:\\Users\\JackZhou\\Documents\\NetBeansProjects\\classloader\\build\\classes" ;? ??????? FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);? ??????? FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);? ??????? String className = "com.example.Sample" ;? ??????? try {? ??????????? Class<?> class1 = fscl1.loadClass(className);? ? ??????????? Object obj1 = class1.newInstance();? ? ??????????? Class<?> class2 = fscl2.loadClass(className);? ??????????? Object obj2 = class2.newInstance();? ??????????? Method setSampleMethod = class1.getMethod("setSample" , java.lang.Object.class );? ??????????? setSampleMethod.invoke(obj1, obj2);? ??????? } catch (Exception e) {? ??????????? e.printStackTrace();? ??????? }? ??? }? }?
package classloader;import java.lang.reflect.Method;public class ClassIdentity {public static void main(String[] args) {new ClassIdentity().testClassIdentity();}public void testClassIdentity() {String classDataRootPath = "C:\\Users\\JackZhou\\Documents\\NetBeansProjects\\classloader\\build\\classes";FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);String className = "com.example.Sample";try {Class<?> class1 = fscl1.loadClass(className); // 加載Sample類Object obj1 = class1.newInstance(); // 創(chuàng)建對象Class<?> class2 = fscl2.loadClass(className);Object obj2 = class2.newInstance();Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);setSampleMethod.invoke(obj1, obj2);} catch (Exception e) {e.printStackTrace();}}
}
運(yùn)行輸出:com.example.Sample@7852e922
5.2 網(wǎng)絡(luò)類加載器
下面將通過一個網(wǎng)絡(luò)類加載器來說明如何通過類加載器來實(shí)現(xiàn)組件的動態(tài)更新。即基本的場景是:Java 字節(jié)代碼(.class)文件存放在服務(wù)器上,客戶端通過網(wǎng)絡(luò)的方式獲取字節(jié)代碼并執(zhí)行。當(dāng)有版本更新的時候,只需要替換掉服務(wù)器上保存的文件即可。通過類加載器可以比較簡單的實(shí)現(xiàn)這種需求。 類 NetworkClassLoader負(fù)責(zé)通過網(wǎng)絡(luò)下載Java類字節(jié)代碼并定義出Java類。它的實(shí)現(xiàn)與FileSystemClassLoader類似。
[java] view plaincopyprint?
package classloader;? ? import java.io.ByteArrayOutputStream;? import java.io.InputStream;? import java.net.URL;? ? public class NetworkClassLoader extends ClassLoader {? ? ??? private String rootUrl;? ? ??? public NetworkClassLoader(String rootUrl) {? ??????? ? ??????? this .rootUrl = rootUrl;? ??? }? ? ??? ? ??? @Override ? ??? 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 {? ??????????? URL url = new URL(path);? ??????????? InputStream ins = url.openStream();? ??????????? 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 (Exception e) {? ??????????? e.printStackTrace();? ??????? }? ??????? return null ;? ??? }? ? ??? private String classNameToPath(String className) {? ??????? ? ??????? return rootUrl + "/" ? ??????????????? + className.replace('.' , '/' ) + ".class" ;? ??? }? }?
package classloader;import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;public class NetworkClassLoader extends ClassLoader {private String rootUrl;public NetworkClassLoader(String rootUrl) {// 指定URLthis.rootUrl = rootUrl;}// 獲取類的字節(jié)碼@Overrideprotected 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) {// 從網(wǎng)絡(luò)上讀取的類的字節(jié)String path = classNameToPath(className);try {URL url = new URL(path);InputStream ins = url.openStream();ByteArrayOutputStream baos = new ByteArrayOutputStream();int bufferSize = 4096;byte[] buffer = new byte[bufferSize];int bytesNumRead = 0;// 讀取類文件的字節(jié)while ((bytesNumRead = ins.read(buffer)) != -1) {baos.write(buffer, 0, bytesNumRead);}return baos.toByteArray();} catch (Exception e) {e.printStackTrace();}return null;}private String classNameToPath(String className) {// 得到類文件的URLreturn rootUrl + "/"+ className.replace('.', '/') + ".class";}
}
在通過NetworkClassLoader加載了某個版本的類之后,一般有兩種做法來使用它。第一種做法是使用Java反射API。另外一種做法是使用接口。需要注意的是,并不能直接在客戶端代碼中引用從服務(wù)器上下載的類,因?yàn)榭蛻舳舜a的類加載器找不到這些類。使用Java反射API可以直接調(diào)用Java類的方法。而使用接口的做法則是把接口的類放在客戶端中,從服務(wù)器上加載實(shí)現(xiàn)此接口的不同版本的類。 在客戶端通過相同的接口來使用這些實(shí)現(xiàn)類。我們使用接口的方式。示例如下:
客戶端接口:
[java] view plaincopyprint?
package classloader;? ? public interface Versioned {? ? ??? String getVersion();? }?
package classloader;public interface Versioned {String getVersion();
}
[java] view plaincopyprint?
package classloader;? ? public interface ICalculator extends Versioned {? ? ??? String calculate(String expression);? }?
package classloader;public interface ICalculator extends Versioned {String calculate(String expression);
}
網(wǎng)絡(luò)上的不同版本的類:
[java] view plaincopyprint?
package com.example;? ? import classloader.ICalculator;? ? public class CalculatorBasic implements ICalculator {? ? ??? @Override ? ??? public String calculate(String expression) {? ??????? return expression;? ??? }? ? ??? @Override ? ??? public String getVersion() {? ??????? return "1.0" ;? ??? }? ? }?
package com.example;import classloader.ICalculator;public class CalculatorBasic implements ICalculator {@Overridepublic String calculate(String expression) {return expression;}@Overridepublic String getVersion() {return "1.0";}}
[java] view plaincopyprint?
package com.example;? ? import classloader.ICalculator;? ? public class CalculatorAdvanced implements ICalculator {? ? ??? @Override ? ??? public String calculate(String expression) {? ??????? return "Result is " + expression;? ??? }? ? ??? @Override ? ??? public String getVersion() {? ??????? return "2.0" ;? ??? }? ? }?
package com.example;import classloader.ICalculator;public class CalculatorAdvanced implements ICalculator {@Overridepublic String calculate(String expression) {return "Result is " + expression;}@Overridepublic String getVersion() {return "2.0";}}
在客戶端加載網(wǎng)絡(luò)上的類的過程:
[java] view plaincopyprint?
package classloader;? ? public class CalculatorTest {? ? ??? public static void main(String[] args) {? ??????? String url = "http://localhost:8080/ClassloaderTest/classes" ;? ??????? NetworkClassLoader ncl = new NetworkClassLoader(url);? ??????? String basicClassName = "com.example.CalculatorBasic" ;? ??????? String advancedClassName = "com.example.CalculatorAdvanced" ;? ??????? try {? ??????????? Class<?> clazz = ncl.loadClass(basicClassName);? ? ??????????? ICalculator calculator = (ICalculator) clazz.newInstance();? ? ??????????? System.out.println(calculator.getVersion());? ??????????? clazz = ncl.loadClass(advancedClassName);? ? ??????????? calculator = (ICalculator) clazz.newInstance();? ??????????? System.out.println(calculator.getVersion());? ??????? } catch (Exception e) {? ??????????? e.printStackTrace();? ??????? }? ??? }? ? }?
package classloader;public class CalculatorTest {public static void main(String[] args) {String url = "http://localhost:8080/ClassloaderTest/classes";NetworkClassLoader ncl = new NetworkClassLoader(url);String basicClassName = "com.example.CalculatorBasic";String advancedClassName = "com.example.CalculatorAdvanced";try {Class<?> clazz = ncl.loadClass(basicClassName); // 加載一個版本的類ICalculator calculator = (ICalculator) clazz.newInstance(); // 創(chuàng)建對象System.out.println(calculator.getVersion());clazz = ncl.loadClass(advancedClassName); // 加載另一個版本的類calculator = (ICalculator) clazz.newInstance();System.out.println(calculator.getVersion());} catch (Exception e) {e.printStackTrace();}}}
?
參考文獻(xiàn):
http://www.blogjava.net/zhuxing/archive/2008/08/08/220841.html
http://www.ibm.com/developerworks/cn/java/j-lo-classloader/
1 線程上下文類加載器
線程上下文類加載器(context class loader)是從 JDK 1.2 開始引入的。類 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用來獲取和設(shè)置線程的上下文類加載器。如果沒有通過 setContextClassLoader(ClassLoader cl)方法進(jìn)行設(shè)置的話,線程將繼承其父線程的上下文類加載器。Java 應(yīng)用運(yùn)行的初始線程的上下文類加載器是系統(tǒng)類加載器。在線程中運(yùn)行的代碼可以通過此類加載器來加載類和資源。
前面提到的類加載器的代理模式并不能解決 Java 應(yīng)用開發(fā)中會遇到的類加載器的全部問題。Java 提供了很多服務(wù)提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實(shí)現(xiàn)。常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的接口由 Java 核心庫來提供,如 JAXP 的 SPI 接口定義包含在 javax.xml.parsers包中。這些 SPI 的實(shí)現(xiàn)代碼很可能是作為 Java 應(yīng)用所依賴的 jar 包被包含進(jìn)來,可以通過類路徑(CLASSPATH)來找到,如實(shí)現(xiàn)了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代碼經(jīng)常需要加載具體的實(shí)現(xiàn)類。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory類中的 newInstance()方法用來生成一個新的 DocumentBuilderFactory的實(shí)例。這里的實(shí)例的真正的類是繼承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的實(shí)現(xiàn)所提供的。如在 Apache Xerces 中,實(shí)現(xiàn)的類是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而問題在于,SPI 的接口是 Java 核心庫的一部分,是由引導(dǎo)類加載器來加載的;SPI 實(shí)現(xiàn)的 Java 類一般是由系統(tǒng)類加載器來加載的。引導(dǎo)類加載器是無法找到 SPI 的實(shí)現(xiàn)類的,因?yàn)樗患虞d Java 的核心庫。它也不能代理給系統(tǒng)類加載器,因?yàn)樗窍到y(tǒng)類加載器的祖先類加載器。也就是說,類加載器的代理模式無法解決這個問題。
線程上下文類加載器正好解決了這個問題。如果不做任何的設(shè)置,Java 應(yīng)用的線程的上下文類加載器默認(rèn)就是系統(tǒng)上下文類加載器。在 SPI 接口的代碼中使用線程上下文類加載器,就可以成功的加載到 SPI 實(shí)現(xiàn)的類。線程上下文類加載器在很多 SPI 的實(shí)現(xiàn)中都會用到。
Java默認(rèn)的線程上下文類加載器是系統(tǒng)類加載器(AppClassLoader)。以下代碼摘自sun.misc.Launch的無參構(gòu)造函數(shù)Launch()。
[java] view plaincopyprint?
? try {? ??? loader = AppClassLoader.getAppClassLoader(extcl);? } catch (IOException e) {? ??? throw new InternalError(? "Could not create application class loader" );? }? ? ? Thread.currentThread().setContextClassLoader(loader);?
// Now create the class loader to use to launch the application
try {loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {throw new InternalError(
"Could not create application class loader" );
}// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);
使用線程上下文類加載器,可以在執(zhí)行線程中拋棄雙親委派加載鏈模式,使用線程上下文里的類加載器加載類。 典型的例子有:通過線程上下文來加載第三方庫jndi實(shí)現(xiàn),而不依賴于雙親委派。大部分java application服務(wù)器(jboss, tomcat..)也是采用contextClassLoader來處理web服務(wù)。還有一些采用hot swap特性的框架,也使用了線程上下文類加載器,比如 seasar (full stack framework in japenese)。 線程上下文從根本解決了一般應(yīng)用不能違背雙親委派模式的問題。使java類加載體系顯得更靈活。隨著多核時代的來臨,相信多線程開發(fā)將會越來越多地進(jìn)入程序員的實(shí)際編碼過程中。因此,在編寫基礎(chǔ)設(shè)施時, 通過使用線程上下文來加載類,應(yīng)該是一個很好的選擇。 當(dāng)然,好東西都有利弊。使用線程上下文加載類,也要注意保證多個需要通信的線程間的類加載器應(yīng)該是同一個,防止因?yàn)椴煌念惣虞d器導(dǎo)致類型轉(zhuǎn)換異常(ClassCastException)。
defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)是java.lang.Classloader提供給開發(fā)人員,用來自定義加載class的接口。使用該接口,可以動態(tài)的加載class文件。例如在jdk中,URLClassLoader是配合findClass方法來使用defineClass,可以從網(wǎng)絡(luò)或硬盤上加載class。而使用類加載接口,并加上自己的實(shí)現(xiàn)邏輯,還可以定制出更多的高級特性。
下面是一個簡單的hot swap類加載器實(shí)現(xiàn)。hot swap即熱插拔的意思,這里表示一個類已經(jīng)被一個加載器加載了以后,在不卸載它的情況下重新再加載它一次。我們知道Java缺省的加載器對相同全名的類只會加載一次,以后直接從緩存中取這個Class object。因此要實(shí)現(xiàn)hot swap,必須在加載的那一刻進(jìn)行攔截,先判斷是否已經(jīng)加載,若是則重新加載一次,否則直接首次加載它。 我們從URLClassLoader繼承,加載類的過程都代理給系統(tǒng)類加載器URLClassLoader中的相應(yīng)方法來完成。
[java] view plaincopyprint?
package classloader;? ? import java.net.URL;? import java.net.URLClassLoader;? ? ? public class HotSwapClassLoader extends URLClassLoader {? ? ??? public HotSwapClassLoader(URL[] urls) {? ??????? super (urls);? ??? }? ? ??? public HotSwapClassLoader(URL[] urls, ClassLoader parent) {? ??????? super (urls, parent);? ??? }? ? ??? ? ??? ? ??? public Class<?> load(String name) throws ClassNotFoundException {? ??????? return load(name, false );? ??? }? ? ??? public Class<?> load(String name, boolean resolve) throws ClassNotFoundException {? ??????? ? ??????? if (null != super .findLoadedClass(name)) {? ??????????? return reload(name, resolve);? ??????? }? ??????? ? ??????? Class<?> clazz = super .findClass(name);? ??????? if (resolve) {? ??????????? super .resolveClass(clazz);? ??????? }? ??????? return clazz;? ??? }? ? ??? public Class<?> reload(String name, boolean resolve) throws ClassNotFoundException {? ??????? return new HotSwapClassLoader(super .getURLs(), super .getParent()).load(? ??????????????? name, resolve);? ??? }? }?
package classloader;import java.net.URL;
import java.net.URLClassLoader;/*** 可以重新載入同名類的類加載器實(shí)現(xiàn)* 放棄了雙親委派的加載鏈模式,需要外部維護(hù)重載后的類的成員變量狀態(tài)*/
public class HotSwapClassLoader extends URLClassLoader {public HotSwapClassLoader(URL[] urls) {super(urls);}public HotSwapClassLoader(URL[] urls, ClassLoader parent) {super(urls, parent);}// 下面的兩個重載load方法實(shí)現(xiàn)類的加載,仿照ClassLoader中的兩個loadClass()// 具體的加載過程代理給父類中的相應(yīng)方法來完成public Class<?> load(String name) throws ClassNotFoundException {return load(name, false);}public Class<?> load(String name, boolean resolve) throws ClassNotFoundException {// 若類已經(jīng)被加載,則重新再加載一次if (null != super.findLoadedClass(name)) {return reload(name, resolve);}// 否則用findClass()首次加載它Class<?> clazz = super.findClass(name);if (resolve) {super.resolveClass(clazz);}return clazz;}public Class<?> reload(String name, boolean resolve) throws ClassNotFoundException {return new HotSwapClassLoader(super.getURLs(), super.getParent()).load(name, resolve);}
}
兩個重載的load方法參數(shù)與ClassLoader類中的兩個loadClass()相似。在load的實(shí)現(xiàn)中,用findLoadedClass()查找指定的類是否已經(jīng)被祖先加載器加載了,若已加載則重新再加載一次,從而放棄了雙親委派的方式(這種方式只會加載一次)。若沒有加載則用自身的findClass()來首次加載它。
下面是使用示例:
[java] view plaincopyprint?
package classloader;? ? public class A {? ????? ??? private B b;? ? ??? public void setB(B b) {? ??????? this .b = b;? ??? }? ? ??? public B getB() {? ??????? return b;? ??? }? }?
package classloader;public class A {private B b;public void setB(B b) {this.b = b;}public B getB() {return b;}
}
[java] view plaincopyprint?
package classloader;? ? public class B {? ????? }?
package classloader;public class B {}
[java] view plaincopyprint?
package classloader;? ? import java.lang.reflect.InvocationTargetException;? import java.lang.reflect.Method;? import java.net.MalformedURLException;? import java.net.URL;? ? public class TestHotSwap {? ? ??? public static void main(String args[]) throws MalformedURLException {? ??????? A a = new A();? ? ??????? B b = new B();? ? ??????? a.setB(b);? ? ??????? System.out.printf("A classLoader is %s\n" , a.getClass().getClassLoader());? ??????? System.out.printf("B classLoader is %s\n" , b.getClass().getClassLoader());? ??????? System.out.printf("A.b classLoader is %s\n" , a.getB().getClass().getClassLoader());? ? ??????? try {? ??????????? URL[] urls = new URL[]{ new URL("file:///C:/Users/JackZhou/Documents/NetBeansProjects/classloader/build/classes/" ) };? ??????????? HotSwapClassLoader c1 = new HotSwapClassLoader(urls, a.getClass().getClassLoader());? ??????????? Class clazz = c1.load("classloader.A" );? ? ??????????? Object aInstance = clazz.newInstance();? ? ??????????? Method method1 = clazz.getMethod("setB" , B.class );? ? ??????????? method1.invoke(aInstance, b);??? ? ??????????? Method method2 = clazz.getMethod("getB" );? ? ??????????? Object bInstance = method2.invoke(aInstance);? ? ??????????? System.out.printf("Reloaded A.b classLoader is %s\n" , bInstance.getClass().getClassLoader());? ??????? } catch (MalformedURLException | ClassNotFoundException |?? ??????????????? InstantiationException | IllegalAccessException |?? ??????????????? NoSuchMethodException | SecurityException |?? ??????????????? IllegalArgumentException | InvocationTargetException e) {? ??????????? e.printStackTrace();? ??????? }? ??? }? }?
package classloader;import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;public class TestHotSwap {public static void main(String args[]) throws MalformedURLException {A a = new A(); // 加載類AB b = new B(); // 加載類Ba.setB(b); // A引用了B,把b對象拷貝到A.bSystem.out.printf("A classLoader is %s\n", a.getClass().getClassLoader());System.out.printf("B classLoader is %s\n", b.getClass().getClassLoader());System.out.printf("A.b classLoader is %s\n", a.getB().getClass().getClassLoader());try {URL[] urls = new URL[]{ new URL("file:///C:/Users/JackZhou/Documents/NetBeansProjects/classloader/build/classes/") };HotSwapClassLoader c1 = new HotSwapClassLoader(urls, a.getClass().getClassLoader());Class clazz = c1.load("classloader.A"); // 用hot swap重新加載類AObject aInstance = clazz.newInstance(); // 創(chuàng)建A類對象Method method1 = clazz.getMethod("setB", B.class); // 獲取setB(B b)方法method1.invoke(aInstance, b); // 調(diào)用setB(b)方法,重新把b對象拷貝到A.bMethod method2 = clazz.getMethod("getB"); // 獲取getB()方法Object bInstance = method2.invoke(aInstance); // 調(diào)用getB()方法System.out.printf("Reloaded A.b classLoader is %s\n", bInstance.getClass().getClassLoader());} catch (MalformedURLException | ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException e) {e.printStackTrace();}}
}
運(yùn)行輸出:
[java] view plaincopyprint?
A classLoader is sun.misc.Launcher$AppClassLoader@73d16e93 ? B classLoader is sun.misc.Launcher$AppClassLoader@73d16e93 ? A.b classLoader is sun.misc.Launcher$AppClassLoader@73d16e93 ? Reloaded A.b classLoader is sun.misc.Launcher$AppClassLoader@73d16e93 ?
A classLoader is sun.misc.Launcher$AppClassLoader@73d16e93
B classLoader is sun.misc.Launcher$AppClassLoader@73d16e93
A.b classLoader is sun.misc.Launcher$AppClassLoader@73d16e93
Reloaded A.b classLoader is sun.misc.Launcher$AppClassLoader@73d16e93
HotSwapClassLoader加載器的作用是重新加載同名的類。為了實(shí)現(xiàn)hot swap,一個類在加載過后,若重新再加載一次,則新的Class object的狀態(tài)會改變,老的狀態(tài)數(shù)據(jù)需要通過其他方式拷貝到重新加載過的類生成的全新Class object實(shí)例中來。上面A類引用了B類,加載A時也會加載B(如果B已經(jīng)加載,則直接從緩存中取出)。在重新加載A后,其Class object中的成員b會重置,因此要重新調(diào)用setB(b)拷貝一次。你可以注釋掉這行代碼,再運(yùn)行會拋出java.lang.NullPointerException,指示A.b為null。 注意新的A Class object實(shí)例所依賴的B類Class object,如果它與老的B Class object實(shí)例不是同一個類加載器加載的, 將會拋出類型轉(zhuǎn)換異常(ClassCastException),表示兩種不同的類。因此在重新加載A后,要特別注意給它的B類成員b傳入外部值時,它們是否由同一個類加載器加載。為了解決這種問題, HotSwapClassLoader自定義的l/oad方法中,當(dāng)前類(類A)是由自身classLoader加載的, 而內(nèi)部依賴的類(類B)還是老對象的classLoader加載的。
2 何時使用Thread.getContextClassLoader()?
這是一個很常見的問題,但答案卻很難回答。這個問題通常在需要動態(tài)加載類和資源的系統(tǒng)編程時會遇到。總的說來動態(tài)加載資源時,往往需要從三種類加載器里選擇:系統(tǒng)或程序的類加載器、當(dāng)前類加載器、以及當(dāng)前線程的上下文類加載器。在程序中應(yīng)該使用何種類加載器呢? 系統(tǒng)類加載器通常不會使用。此類加載器處理啟動應(yīng)用程序時classpath指定的類,可以通過ClassLoader.getSystemClassLoader()來獲得。所有的ClassLoader.getSystemXXX()接口也是通過這個類加載器加載的。一般不要顯式調(diào)用這些方法,應(yīng)該讓其他類加載器代理到系統(tǒng)類加載器上。 由于系統(tǒng)類加載器是JVM最后創(chuàng)建的類加載器,這樣代碼只會適應(yīng)于簡單命令行啟動的程序。一旦代碼移植到EJB、Web應(yīng)用或者Java Web Start應(yīng)用程序中,程序肯定不能正確執(zhí)行。 因此一般只有兩種選擇,當(dāng)前類加載器和線程上下文類加載器。當(dāng)前類加載器是指當(dāng)前方法所在類的加載器。這個類加載器是運(yùn)行時類解析使用的加載器,Class.forName(String)和Class.getResource(String)也使用該類加載器。代碼中X.class的寫法使用的類加載器也是這個類加載器。 線程上下文類加載器在Java 2(J2SE)時引入。每個線程都有一個關(guān)聯(lián)的上下文類加載器。如果你使用new Thread()方式生成新的線程,新線程將繼承其父線程的上下文類加載器。如果程序?qū)€程上下文類加載器沒有任何改動的話,程序中所有的線程將都使用系統(tǒng)類加載器作為上下文類加載器。Web應(yīng)用和Java企業(yè)級應(yīng)用中,應(yīng)用服務(wù)器經(jīng)常要使用復(fù)雜的類加載器結(jié)構(gòu)來實(shí)現(xiàn)JNDI(Java命名和目錄接口)、線程池、組件熱部署等功能,因此理解這一點(diǎn)尤其重要。 為什么要引入線程的上下文類加載器? 將它引入J2SE并不是純粹的噱頭,由于Sun沒有提供充分的文檔解釋說明這一點(diǎn),這使許多開發(fā)者很糊涂。實(shí)際上,上下文類加載器為同樣在J2SE中引入的類加載代理機(jī)制提供了后門。 通常JVM中的類加載器是按照層次結(jié)構(gòu)組織的,目的是每個類加載器(除了啟動整個JVM的原初類加載器)都有一個父類加載器。當(dāng)類加載請求到來時,類加載器通常首先將請求代理給父類加載器。只有當(dāng)父類加載器失敗后,它才試圖按照自己的算法查找并定義當(dāng)前類。 有時這種模式并不能總是奏效。這通常發(fā)生在JVM核心代碼必須動態(tài)加載由應(yīng)用程序動態(tài)提供的資源時。拿JNDI為例,它的核心是由JRE核心類(rt.jar)實(shí)現(xiàn)的。但這些核心JNDI類必須能加載由第三方廠商提供的JNDI實(shí)現(xiàn)。這種情況下調(diào)用父類加載器(原初類加載器)來加載只有其子類加載器可見的類,這種代理機(jī)制就會失效。解決辦法就是讓核心JNDI類使用線程上下文類加載器,從而有效的打通類加載器層次結(jié)構(gòu),逆著代理機(jī)制的方向使用類加載器。 順便提一下,XML解析API(JAXP)也是使用此種機(jī)制。當(dāng)JAXP還是J2SE擴(kuò)展時,XML解析器使用當(dāng)前類加載器方法來加載解析器實(shí)現(xiàn)。但當(dāng)JAXP成為J2SE核心代碼后,類加載機(jī)制就換成了使用線程上下文加載器,這和JNDI的原因相似。 好了,現(xiàn)在我們明白了問題的關(guān)鍵:這兩種選擇不可能適應(yīng)所有情況。一些人認(rèn)為線程上下文類加載器應(yīng)成為新的標(biāo)準(zhǔn)。但這在不同JVM線程共享數(shù)據(jù)來溝通時,就會使類加載器的結(jié)構(gòu)亂七八糟。除非所有線程都使用同一個上下文類加載器。而且,使用當(dāng)前類加載器已成為缺省規(guī)則,它們廣泛應(yīng)用在類聲明、Class.forName等情景中。即使你想盡可能只使用上下文類加載器,總是有這樣那樣的代碼不是你所能控制的。這些代碼都使用代理到當(dāng)前類加載器的模式。混雜使用代理模式是很危險的。 更為糟糕的是,某些應(yīng)用服務(wù)器將當(dāng)前類加載器和上下文類加器分別設(shè)置成不同的ClassLoader實(shí)例。雖然它們擁有相同的類路徑,但是它們之間并不存在父子代理關(guān)系。想想這為什么可怕:記住加載并定義某個類的類加載器是虛擬機(jī)內(nèi)部標(biāo)識該類的組成部分,如果當(dāng)前類加載器加載類X并接著執(zhí)行它,如JNDI查找類型為Y的數(shù)據(jù),上下文類加載器能夠加載并定義Y,這個Y的定義和當(dāng)前類加載器加載的相同名稱的類就不是同一個,使用隱式類型轉(zhuǎn)換就會造成異常。 這種混亂的狀況還將在Java中存在很長時間。在J2SE中還包括以下的功能使用不同的類加載器: (1)JNDI使用線程上下文類加載器。 (2)Class.getResource()和Class.forName()使用當(dāng)前類加載器。 (3)JAXP使用上下文類加載器。 (4)java.util.ResourceBundle使用調(diào)用者的當(dāng)前類加載器。 (5)URL協(xié)議處理器使用java.protocol.handler.pkgs系統(tǒng)屬性并只使用系統(tǒng)類加載器。 (6)Java序列化API缺省使用調(diào)用者當(dāng)前的類加載器。 這些類加載器非常混亂,沒有在J2SE文檔中給以清晰明確的說明。 該如何選擇類加載器? 如若代碼是限于某些特定框架,這些框架有著特定加載規(guī)則,則不要做任何改動,讓框架開發(fā)者來保證其工作(比如應(yīng)用服務(wù)器提供商,盡管他們并不能總是做對)。如在Web應(yīng)用和EJB中,要使用Class.gerResource來加載資源。
在其他情況下,我們可以自己來選擇最合適的類加載器。可以使用策略模式來設(shè)計選擇機(jī)制。其思想是將 “ 總是使用上下文類加載器 ” 或者 “ 總是使用當(dāng)前類加載器 ” 的決策同具體實(shí)現(xiàn)邏輯分離開。往往設(shè)計之初是很難預(yù)測何種類加載策略是合適的,該設(shè)計能夠讓你可以后來修改類加載策略。
考慮使用下面的代碼,這是作者本人在工作中發(fā)現(xiàn)的經(jīng)驗(yàn)。這兒有一個缺省實(shí)現(xiàn),應(yīng)該可以適應(yīng)大部分工作場景:
[java] view plaincopyprint?
package classloader.context;? ? ? public class ClassLoadContext {? ? ??? private final Class m_caller;? ? ??? public final Class getCallerClass() {? ??????? return m_caller;? ??? }? ? ??? ClassLoadContext(final Class caller) {? ??????? m_caller = caller;? ??? }? }?
package classloader.context;/*** 類加載上下文,持有要加載的類*/
public class ClassLoadContext {private final Class m_caller;public final Class getCallerClass() {return m_caller;}ClassLoadContext(final Class caller) {m_caller = caller;}
}
?
[java] view plaincopyprint?
package classloader.context;? ? ? public interface IClassLoadStrategy {? ? ??? ClassLoader getClassLoader(ClassLoadContext ctx);? }?
package classloader.context;/*** 類加載策略接口*/
public interface IClassLoadStrategy {ClassLoader getClassLoader(ClassLoadContext ctx);
}
[java] view plaincopyprint?
? public class DefaultClassLoadStrategy implements IClassLoadStrategy {? ? ??? ? ??? @Override ? ??? public ClassLoader getClassLoader(final ClassLoadContext ctx) {? ??????? final ClassLoader callerLoader = ctx.getCallerClass().getClassLoader();? ??????? final ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();? ??????? ClassLoader result;? ? ??????? ? ??????? ? ??????? if (isChild(contextLoader, callerLoader)) {? ??????????? result = callerLoader;? ??????? } else if (isChild(callerLoader, contextLoader)) {? ??????????? result = contextLoader;? ??????? } else {? ??????????? ? ??????????? ? ??????????? result = contextLoader;? ??????? }? ??????? final ClassLoader systemLoader = ClassLoader.getSystemClassLoader();? ??????? ? ??????? if (isChild(result, systemLoader)) {? ??????????? result = systemLoader;? ??????? }? ????????? ??????? return result;? ??? }? ????? ??? ? ??? private boolean isChild(ClassLoader oneLoader, ClassLoader anotherLoader){? ??????? ? ??? }? ? ??? ? }?
/*** 缺省的類加載策略,可以適應(yīng)大部分工作場景*/
public class DefaultClassLoadStrategy implements IClassLoadStrategy {/*** 為ctx返回最合適的類加載器,從系統(tǒng)類加載器、當(dāng)前類加載器* 和當(dāng)前線程上下文類加載中選擇一個最底層的加載器* @param ctx* @return */@Overridepublic ClassLoader getClassLoader(final ClassLoadContext ctx) {final ClassLoader callerLoader = ctx.getCallerClass().getClassLoader();final ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();ClassLoader result;// If 'callerLoader' and 'contextLoader' are in a parent-child// relationship, always choose the child:if (isChild(contextLoader, callerLoader)) {result = callerLoader;} else if (isChild(callerLoader, contextLoader)) {result = contextLoader;} else {// This else branch could be merged into the previous one,// but I show it here to emphasize the ambiguous case:result = contextLoader;}final ClassLoader systemLoader = ClassLoader.getSystemClassLoader();// Precaution for when deployed as a bootstrap or extension class:if (isChild(result, systemLoader)) {result = systemLoader;}return result;}// 判斷anotherLoader是否是oneLoader的childprivate boolean isChild(ClassLoader oneLoader, ClassLoader anotherLoader){//...}// ... more methods
}
決定應(yīng)該使用何種類加載器的接口是IClassLoaderStrategy,為了幫助IClassLoadStrategy做決定,給它傳遞了個ClassLoadContext對象作為參數(shù)。ClassLoadContext持有要加載的類。
上面代碼的邏輯很簡單:如調(diào)用類的當(dāng)前類加載器和上下文類加載器是父子關(guān)系,則總是選擇子類加載器。對子類加載器可見的資源通常是對父類可見資源的超集,因此如果每個開發(fā)者都遵循J2SE的代理規(guī)則,這樣做大多數(shù)情況下是合適的。 當(dāng)前類加載器和上下文類加載器是兄弟關(guān)系時,決定使用哪一個是比較困難的。理想情況下,Java運(yùn)行時不應(yīng)產(chǎn)生這種模糊。但一旦發(fā)生,上面代碼選擇上下文類加載器。這是作者本人的實(shí)際經(jīng)驗(yàn),絕大多數(shù)情況下應(yīng)該能正常工作。你可以修改這部分代碼來適應(yīng)具體需要。一般來說,上下文類加載器要比當(dāng)前類加載器更適合于框架編程,而當(dāng)前類加載器則更適合于業(yè)務(wù)邏輯編程。 最后需要檢查一下,以便保證所選類加載器不是系統(tǒng)類加載器的父親,在開發(fā)標(biāo)準(zhǔn)擴(kuò)展類庫時這通常是個好習(xí)慣。 注意作者故意沒有檢查要加載資源或類的名稱。Java XML API成為J2SE核心的歷程應(yīng)該能讓我們清楚過濾類名并不是好想法。作者也沒有試圖檢查哪個類加載器加載首先成功,而是檢查類加載器的父子關(guān)系,這是更好更有保證的方法。 下面是類加載器的選擇器:
[java] view plaincopyprint?
package classloader.context;? ? ? public abstract class ClassLoaderResolver {? ????????? ??? private static IClassLoadStrategy s_strategy;? ? ??? private static final int CALL_CONTEXT_OFFSET = 3 ;? ? ??? private static final CallerResolver CALLER_RESOLVER;? ? ????? ??? static {? ??????? try {? ??????????? ? ??????????? ? ??????????? CALLER_RESOLVER = new CallerResolver();? ??????? } catch (SecurityException se) {? ??????????? throw new RuntimeException("ClassLoaderResolver: could not create CallerResolver: " + se);? ??????? }? ??????? s_strategy = new DefaultClassLoadStrategy();? ? ??? }? ? ??? ? ??? public static synchronized ClassLoader getClassLoader() {? ??????? final Class caller = getCallerClass(0 ); ? ??????? final ClassLoadContext ctx = new ClassLoadContext(caller);? ? ??????? return s_strategy.getClassLoader(ctx);? ? ??? }? ? ??? public static synchronized IClassLoadStrategy getStrategy() {? ??????? return s_strategy;? ??? }? ? ??? public static synchronized IClassLoadStrategy setStrategy(final IClassLoadStrategy strategy) {? ??????? final IClassLoadStrategy old = s_strategy;? ? ??????? s_strategy = strategy;? ??????? return old;? ??? }? ? ??? ? ??? private static final class CallerResolver extends SecurityManager {? ??????? @Override ? ??????? protected Class[] getClassContext() {? ??????????? return super .getClassContext();? ? ??????? }? ? ??? }? ? ??? ? ??? private static Class getCallerClass(final int callerOffset) {? ??????? return CALLER_RESOLVER.getClassContext()[CALL_CONTEXT_OFFSET? ??????????????? + callerOffset];? ? ??? }? }?
package classloader.context;/*** 類加載解析器,獲取最合適的類加載器*/
public abstract class ClassLoaderResolver {private static IClassLoadStrategy s_strategy; // initialized in <clinit>private static final int CALL_CONTEXT_OFFSET = 3; // may need to change if this class is redesignedprivate static final CallerResolver CALLER_RESOLVER; // set in <clinit>static {try {// This can fail if the current SecurityManager does not allow// RuntimePermission ("createSecurityManager"):CALLER_RESOLVER = new CallerResolver();} catch (SecurityException se) {throw new RuntimeException("ClassLoaderResolver: could not create CallerResolver: " + se);}s_strategy = new DefaultClassLoadStrategy(); //默認(rèn)使用缺省加載策略}/*** This method selects the best classloader instance to be used for* class/resource loading by whoever calls this method. The decision* typically involves choosing between the caller's current, thread context,* system, and other classloaders in the JVM and is made by the {@link IClassLoadStrategy}* instance established by the last call to {@link #setStrategy}.* * @return classloader to be used by the caller ['null' indicates the* primordial loader]*/public static synchronized ClassLoader getClassLoader() {final Class caller = getCallerClass(0); // 獲取執(zhí)行當(dāng)前方法的類final ClassLoadContext ctx = new ClassLoadContext(caller); // 創(chuàng)建類加載上下文return s_strategy.getClassLoader(ctx); // 獲取最合適的類加載器}public static synchronized IClassLoadStrategy getStrategy() {return s_strategy;}public static synchronized IClassLoadStrategy setStrategy(final IClassLoadStrategy strategy) {final IClassLoadStrategy old = s_strategy; // 設(shè)置類加載策略s_strategy = strategy;return old;}/*** A helper class to get the call context. It subclasses SecurityManager* to make getClassContext() accessible. An instance of CallerResolver* only needs to be created, not installed as an actual security manager.*/private static final class CallerResolver extends SecurityManager {@Overrideprotected Class[] getClassContext() {return super.getClassContext(); // 獲取當(dāng)執(zhí)行棧的所有類,native方法}}/** Indexes into the current method call context with a given* offset.*/private static Class getCallerClass(final int callerOffset) {return CALLER_RESOLVER.getClassContext()[CALL_CONTEXT_OFFSET+ callerOffset]; // 獲取執(zhí)行棧上某個方法所屬的類}
}
可通過調(diào)用ClassLoaderResolver.getClassLoader() 方法來獲取類加載器對象,并使用其 ClassLoader 的接口如loadClass()等來加載類和資源。此外還可使用下面的 ResourceLoader 接口來取代 ClassLoader 接口:
[java] view plaincopyprint?
package classloader.context;? ? import java.net.URL;? ? public class ResourceLoader {? ? ??? ? ??? public static Class<?> loadClass(final String name) throws ClassNotFoundException {? ??????? ? ??????? final ClassLoader loader = ClassLoaderResolver.getClassLoader();? ??????? ? ??????? return Class.forName(name, false , loader);? ??? }? ? ??? ? ??? public static URL getResource(final String name) {? ??????? ? ??????? final ClassLoader loader = ClassLoaderResolver.getClassLoader();? ??????? ? ??????? if (loader != null ) {? ??????????? return loader.getResource(name);? ??????? } else {? ??????????? return ClassLoader.getSystemResource(name);? ??????? }? ??? }? ? ??? ? }?
package classloader.context;import java.net.URL;public class ResourceLoader {/*** 加載一個類* * @param name* @return * @throws java.lang.ClassNotFoundException * @see java.lang.ClassLoader#loadClass(java.lang.String)*/public static Class<?> loadClass(final String name) throws ClassNotFoundException {//獲取最合適的類加載器final ClassLoader loader = ClassLoaderResolver.getClassLoader();//用指定加載器加載類return Class.forName(name, false, loader);}/*** 加載一個資源* * @param name* @return * @see java.lang.ClassLoader#getResource(java.lang.String)*/public static URL getResource(final String name) {//獲取最合適的類加載器final ClassLoader loader = ClassLoaderResolver.getClassLoader();//查找指定的資源if (loader != null) {return loader.getResource(name);} else {return ClassLoader.getSystemResource(name);}}// ... more methods ...
}
ClassLoadContext.getCallerClass()返回的類在ClassLoaderResolver或ResourceLoader使用,這樣做的目的是讓其能找到調(diào)用類的類加載器(上下文加載器總是能通過Thread.currentThread().getContextClassLoader()來獲得)。注意調(diào)用類是靜態(tài)獲得的,因此這個接口不需現(xiàn)有業(yè)務(wù)方法增加額外的Class參數(shù),而且也適合于靜態(tài)方法和類初始化代碼。具體使用時,可以往這個上下文對象中添加具體部署環(huán)境中所需的其他屬性。
3 類加載器與Web容器
對于運(yùn)行在 Java EE容器中的 Web 應(yīng)用來說,類加載器的實(shí)現(xiàn)方式與一般的 Java 應(yīng)用有所不同。不同的 Web 容器的實(shí)現(xiàn)方式也會有所不同。以 Apache Tomcat 來說,每個 Web 應(yīng)用都有一個對應(yīng)的類加載器實(shí)例。該類加載器也使用代理模式,所不同的是它是首先嘗試去加載某個類,如果找不到再代理給父類加載器。這與一般類加載器的順序是相反的。這是 Java Servlet 規(guī)范中的推薦做法,其目的是使得 Web 應(yīng)用自己的類的優(yōu)先級高于 Web 容器提供的類。 這種代理模式的一個例外是:Java 核心庫的類是不在查找范圍之內(nèi)的。這也是為了保證 Java 核心庫的類型安全。 絕大多數(shù)情況下,Web 應(yīng)用的開發(fā)人員不需要考慮與類加載器相關(guān)的細(xì)節(jié)。下面給出幾條簡單的原則: (1)每個 Web 應(yīng)用自己的 Java 類文件和使用的庫的 jar 包,分別放在 WEB-INF/classes和 WEB-INF/lib目錄下面。 (2)多個應(yīng)用共享的 Java 類文件和 jar 包,分別放在 Web 容器指定的由所有 Web 應(yīng)用共享的目錄下面。 (3)當(dāng)出現(xiàn)找不到類的錯誤時,檢查當(dāng)前類的類加載器和當(dāng)前線程的上下文類加載器是否正確。
4 類加載器與OSGi
OSGi是 Java 上的動態(tài)模塊系統(tǒng)。它為開發(fā)人員提供了面向服務(wù)和基于組件的運(yùn)行環(huán)境,并提供標(biāo)準(zhǔn)的方式用來管理軟件的生命周期。OSGi 已經(jīng)被實(shí)現(xiàn)和部署在很多產(chǎn)品上,在開源社區(qū)也得到了廣泛的支持。Eclipse就是基于OSGi 技術(shù)來構(gòu)建的。 OSGi 中的每個模塊(bundle)都包含 Java 包和類。模塊可以聲明它所依賴的需要導(dǎo)入(import)的其它模塊的 Java 包和類(通過 Import-Package),也可以聲明導(dǎo)出(export)自己的包和類,供其它模塊使用(通過 Export-Package)。也就是說需要能夠隱藏和共享一個模塊中的某些 Java 包和類。這是通過 OSGi 特有的類加載器機(jī)制來實(shí)現(xiàn)的。OSGi 中的每個模塊都有對應(yīng)的一個類加載器。它負(fù)責(zé)加載模塊自己包含的 Java 包和類。當(dāng)它需要加載 Java 核心庫的類時(以 java開頭的包和類),它會代理給父類加載器(通常是啟動類加載器)來完成。當(dāng)它需要加載所導(dǎo)入的 Java 類時,它會代理給導(dǎo)出此 Java 類的模塊來完成加載。 模塊也可以顯式的聲明某些 Java 包和類,必須由父類加載器來加載。只需要設(shè)置系統(tǒng)屬性 org.osgi.framework.bootdelegation的值即可。 假設(shè)有兩個模塊 bundleA 和 bundleB,它們都有自己對應(yīng)的類加載器 classLoaderA 和 classLoaderB。在 bundleA 中包含類 com.bundleA.Sample,并且該類被聲明為導(dǎo)出的,也就是說可以被其它模塊所使用的。bundleB 聲明了導(dǎo)入 bundleA 提供的類 com.bundleA.Sample,并包含一個類 com.bundleB.NewSample繼承自 com.bundleA.Sample。在 bundleB 啟動的時候,其類加載器 classLoaderB 需要加載類 com.bundleB.NewSample,進(jìn)而需要加載類 com.bundleA.Sample。由于 bundleB 聲明了類 com.bundleA.Sample是導(dǎo)入的,classLoaderB 把加載類 com.bundleA.Sample的工作代理給導(dǎo)出該類的 bundleA 的類加載器 classLoaderA。classLoaderA 在其模塊內(nèi)部查找類 com.bundleA.Sample并定義它,所得到的類 com.bundleA.Sample實(shí)例就可以被所有聲明導(dǎo)入了此類的模塊使用。對于以 java開頭的類,都是由父類加載器來加載的。如果聲明了系統(tǒng)屬性 org.osgi.framework.bootdelegation=com.example.core.*,那么對于包 com.example.core中的類,都是由父類加載器來完成的。 OSGi 模塊的這種類加載器結(jié)構(gòu),使得一個類的不同版本可以共存在 Java 虛擬機(jī)中,帶來了很大的靈活性。 不過它的這種不同,也會給開發(fā)人員帶來一些麻煩,尤其當(dāng)模塊需要使用第三方提供的庫的時候。下面提供幾條比較好的建議: (1)如果一個類庫只有一個模塊使用,把該類庫的 jar 包放在模塊中,在 Bundle-ClassPath中指明即可。 (2)如果一個類庫被多個模塊共用,可以為這個類庫單獨(dú)的創(chuàng)建一個模塊,把其它模塊需要用到的 Java 包聲明為導(dǎo)出的。其它模塊聲明導(dǎo)入這些類。 (3)如果類庫提供了 SPI 接口,并且利用線程上下文類加載器來加載 SPI 實(shí)現(xiàn)的 Java 類,有可能會找不到 Java 類。如果出現(xiàn)了 NoClassDefFoundError異常,首先檢查當(dāng)前線程的上下文類加載器是否正確。通過 Thread.currentThread().getContextClassLoader()就可以得到該類加載器。該類加載器應(yīng)該是該模塊對應(yīng)的類加載器。如果不是的話,可以首先通過 class.getClassLoader()來得到模塊對應(yīng)的類加載器,再通過 Thread.currentThread().setContextClassLoader()來設(shè)置當(dāng)前線程的上下文類加載器。
總結(jié)
類加載器是 Java 語言的一個創(chuàng)新。它使得動態(tài)安裝和更新軟件組件成為可能。本文詳細(xì)介紹了類加載器的相關(guān)話題,包括基本概念、代理模式、線程上下文類加載器、與 Web 容器和 OSGi 的關(guān)系等。開發(fā)人員在遇到 ClassNotFoundException和 NoClassDefFoundError等異常的時候,應(yīng)該檢查拋出異常的類的類加載器和當(dāng)前線程的上下文類加載器,從中可以發(fā)現(xiàn)問題的所在。在開發(fā)自己的類加載器的時候,需要注意與已有的類加載器組織結(jié)構(gòu)的協(xié)調(diào)。
參考文獻(xiàn):
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/
http://www.blogjava.net/lihao336/archive/2009/09/17/295489.html
http://kenwublog.com/structure-of-java-class-loader
?
?
總結(jié)
以上是生活随笔 為你收集整理的深入理解Java类加载器:Java类加载原理解析 的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔 推薦給好友。