1 基本信息
每個開發(fā)人員對java.lang.ClassNotFoundExcetpion這個異??隙ǘ疾荒吧?,這背后就涉及到了java技術(shù)體系中的類加載。Java的類加載機制是技術(shù)體系中比較核心的部分,雖然和大部分開發(fā)人員直接打交道不多,但是對其背后的機理有一定理解有助于排查程序中出現(xiàn)的類加載失敗等技術(shù)問題,對理解java虛擬機的連接模型和java語言的動態(tài)性都有很大幫助。
2 Java虛擬機類加載器結(jié)構(gòu)簡述
2.1 JVM三種預定義類型類加載器
我們首先看一下JVM預定義的三種類型類加載器,當一個 JVM啟動的時候,Java缺省開始使用如下三種類型類裝入器:
啟動(Bootstrap)類加載器:引導類裝入器是用本地代碼實現(xiàn)的類裝入器,它負責將 <Java_Runtime_Home>/lib下面的核心類庫或-Xbootclasspath選項指定的jar包加載到內(nèi)存中。由于引導類加載器涉及到虛擬機本地實現(xiàn)細節(jié),開發(fā)者無法直接獲取到啟動類加載器的引用,所以不允許直接通過引用進行操作。
擴展(Extension)類加載器:擴展類加載器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現(xiàn)的。它負責將< Java_Runtime_Home >/lib/ext或者由系統(tǒng)變量-Djava.ext.dir指定位置中的類庫加載到內(nèi)存中。開發(fā)者可以直接使用標準擴展類加載器。
系統(tǒng)(System)類加載器:系統(tǒng)類加載器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現(xiàn)的。它負責將系統(tǒng)類路徑j(luò)ava -classpath或-Djava.class.path變量所指的目錄下的類庫加載到內(nèi)存中。開發(fā)者可以直接使用系統(tǒng)類加載器。
除了以上列舉的三種類加載器,還有一種比較特殊的類型就是線程上下文類加載器,這個將在后面單獨介紹。
2.2 類加載雙親委派機制介紹和分析
在這里,需要著重說明的是,JVM在加載類時默認采用的是**雙親委派**機制。通俗的講,**就是某個特定的類加載器在接到加載類的請求時,首先將加載任務委托給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。**關(guān)于虛擬機默認的雙親委派機制,我們可以從系統(tǒng)類加載器和擴展類加載器為例作簡單分析。
圖一 標準擴展類加載器繼承層次圖
圖二系統(tǒng)類加載器繼承層次圖
通過圖一和圖二我們可以看出,類加載器均是繼承自java.lang.ClassLoader抽象類。我們下面我們就看簡要介紹一下java.lang.ClassLoader中幾個最重要的方法:
[java] view plaincopy
<embed id="ZeroClipboardMovie_1" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_1" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=1&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
//加載指定名稱(包括包名)的二進制類型,供用戶調(diào)用的接口
public Class<?> loadClass(String name) throws ClassNotFoundException{ … }
//加載指定名稱(包括包名)的二進制類型,同時指定是否解析(但是這里的resolve參數(shù)不一定真正能達到解析的效果),供繼承用
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ … }
//findClass方法一般被loadClass方法調(diào)用去加載指定名稱類,供繼承用
protected Class<?> findClass(String name) throws ClassNotFoundException { … }
//定義類型,一般在findClass方法中讀取到對應字節(jié)碼后調(diào)用,可以看出不可繼承
//(說明:JVM已經(jīng)實現(xiàn)了對應的具體功能,解析對應的字節(jié)碼,產(chǎn)生對應的內(nèi)部數(shù)據(jù)結(jié)構(gòu)放置到方法區(qū),所以無需覆寫,直接調(diào)用就可以了)
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ … }
通過進一步分析標準擴展類加載器(sun.misc.Launcher$ExtClassLoader)和系統(tǒng)類加載器(sun.misc.Launcher$AppClassLoader)的代碼以及其公共父類(java.net.URLClassLoader和java.security.SecureClassLoader)的代碼可以看出,都沒有覆寫****java.lang.ClassLoader中默認的加載委派規(guī)則---loadClass(…****)方法。既然這樣,我們就可以通過分析java.lang.ClassLoader中的loadClass(String name)方法的代碼就可以分析出虛擬機默認采用的雙親委派機制到底是什么模樣:
[java] view plaincopy
<embed id="ZeroClipboardMovie_2" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_2" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=2&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
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) {
// 如果父類加載器和啟動類加載器都不能完成加載任務,才調(diào)用自身的加載功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
通過上面的代碼分析,我們可以對JVM采用的雙親委派類加載機制有了更感性的認識,下面我們就接著分析一下啟動類加載器、標準擴展類加載器和系統(tǒng)類加載器三者之間的關(guān)系。可能大家已經(jīng)從各種資料上面看到了如下類似的一幅圖片:
圖三 類加載器默認委派關(guān)系圖
上面圖片給人的直觀印象是系統(tǒng)類加載器的父類加載器是標準擴展類加載器,標準擴展類加載器的父類加載器是啟動類加載器,下面我們就用代碼具體測試一下:
[java] view plaincopy
<embed id="ZeroClipboardMovie_3" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_3" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=3&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
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 plaincopy
<embed id="ZeroClipboardMovie_4" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_4" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=4&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
- sun.misc.Launcher$AppClassLoader@6d06d69c
- sun.misc.Launcher$ExtClassLoader@70dea4e
- null
通過以上的代碼輸出,我們可以判定系統(tǒng)類加載器的父加載器是標準擴展類加載器,但是我們試圖獲取標準擴展類加載器的父類加載器時確得到了****null,就是說標準擴展類加載器本身強制設(shè)定父類加載器為null****。我們還是借助于代碼分析一下。
我們首先看一下java.lang.ClassLoader抽象類中默認實現(xiàn)的兩個構(gòu)造函數(shù):
[java] view plaincopy
<embed id="ZeroClipboardMovie_5" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_5" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=5&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
protected ClassLoader() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
//默認將父類加載器設(shè)置為系統(tǒng)類加載器,getSystemClassLoader()獲取系統(tǒng)類加載器
this.parent = getSystemClassLoader();
initialized = true;
}
protected ClassLoader(ClassLoader parent) {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
//強制設(shè)置父類加載器
this.parent = parent;
initialized = true;
}
我們再看一下ClassLoader抽象類中parent成員的聲明:
[java] view plaincopy
<embed id="ZeroClipboardMovie_6" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_6" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=6&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
- // The parent class loader for delegation
- private ClassLoader parent;
聲明為私有變量的同時并沒有對外提供可供派生類訪問的public或者protected設(shè)置器接口(對應的setter方法),結(jié)合前面的測試代碼的輸出,我們可以推斷出:
1. 系統(tǒng)類加載器(AppClassLoader)調(diào)用ClassLoader(ClassLoader parent)構(gòu)造函數(shù)將父類加載器設(shè)置為標準擴展類加載器(ExtClassLoader)。(因為如果不強制設(shè)置,默認會通過調(diào)用getSystemClassLoader()方法獲取并設(shè)置成系統(tǒng)類加載器,這顯然和測試輸出結(jié)果不符。)
2. 擴展類加載器(ExtClassLoader)調(diào)用ClassLoader(ClassLoader parent)構(gòu)造函數(shù)將父類加載器設(shè)置為null。(因為如果不強制設(shè)置,默認會通過調(diào)用getSystemClassLoader()方法獲取并設(shè)置成系統(tǒng)類加載器,這顯然和測試輸出結(jié)果不符。)
現(xiàn)在我們可能會有這樣的疑問:擴展類加載器(ExtClassLoader)的父類加載器被強制設(shè)置為null了,那么擴展類加載器為什么還能將加載任務委派給啟動類加載器呢?
圖四 標準擴展類加載器和系統(tǒng)類加載器成員大綱視圖
圖五 擴展類加載器和系統(tǒng)類加載器公共父類成員大綱視圖
通過圖四和圖五可以看出,標準擴展類加載器和系統(tǒng)類加載器及其父類(java.net.URLClassLoader和java.security.SecureClassLoader)都沒有覆寫java.lang.ClassLoader中默認的加載委派規(guī)則---loadClass(…)方法。有關(guān)java.lang.ClassLoader中默認的加載委派規(guī)則前面已經(jīng)分析過,如果父加載器為null,則會調(diào)用本地方法進行啟動類加載嘗試。所以,圖三中,啟動類加載器、標準擴展類加載器和系統(tǒng)類加載器之間的委派關(guān)系事實上是仍就成立的。(在后面的用戶自定義類加載器部分,還會做更深入的分析)。
2.3 類加載雙親委派示例
以上已經(jīng)簡要介紹了虛擬機默認使用的啟動類加載器、標準擴展類加載器和系統(tǒng)類加載器,并以三者為例結(jié)合JDK代碼對JVM默認使用的雙親委派類加載機制做了分析。下面我們就來看一個綜合的例子。首先在IDE中建立一個簡單的java應用工程,然后寫一個簡單的JavaBean如下:
[java] view plaincopy
<embed id="ZeroClipboardMovie_7" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_7" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=7&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
package classloader.test.bean;
public class TestBean {
public TestBean() { }
}
在現(xiàn)有當前工程中另外建立一測試類(ClassLoaderTest.java)內(nèi)容如下:
測試一:
[java] view plaincopy
<embed id="ZeroClipboardMovie_8" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_8" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=8&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
package classloader.test.bean;
public class ClassLoaderTest {
public static void main(String[] args) {
try {
//查看當前系統(tǒng)類路徑中包含的路徑條目
System.out.println(System.getProperty("java.class.path"));
//調(diào)用加載當前類的類加載器(這里即為系統(tǒng)類加載器)加載TestBean
Class typeLoaded = Class.forName("classloader.test.bean.TestBean");
//查看被加載的TestBean類型是被那個類加載器加載的
System.out.println(typeLoaded.getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
對應的輸出如下:
[plain] view plaincopy
<embed id="ZeroClipboardMovie_9" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_9" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=9&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
- C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes
- sun.misc.Launcher$AppClassLoader@73d16e93
說明:當前類路徑默認的含有的一個條目就是工程的輸出目錄。
測試二:
將當前工程輸出目錄下的TestBean.class打包進test.jar剪貼到<Java_Runtime_Home>/lib/ext目錄下(現(xiàn)在工程輸出目錄下和JRE擴展目錄下都有待加載類型的class文件)。再運行測試一測試代碼,結(jié)果如下:
[plain] view plaincopy
<embed id="ZeroClipboardMovie_10" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_10" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=10&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
- C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes
- sun.misc.Launcher$ExtClassLoader@15db9742
對比測試一和測試二,我們明顯可以驗證前面說的雙親委派機制,系統(tǒng)類加載器在接到加載classloader.test.bean.TestBean類型的請求時,首先將請求委派給父類加載器(標準擴展類加載器),標準擴展類加載器搶先完成了加載請求。
測試三:
將test.jar拷貝一份到<Java_Runtime_Home>/lib下,運行測試代碼,輸出如下:
[plain] view plaincopy
<embed id="ZeroClipboardMovie_11" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_11" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=11&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
- C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes
- sun.misc.Launcher$ExtClassLoader@15db9742
測試三和測試二輸出結(jié)果一致。那就是說,放置到<Java_Runtime_Home>/lib目錄下的TestBean對應的class字節(jié)碼并沒有被加載,這其實和前面講的雙親委派機制并不矛盾。虛擬機出于安全等因素考慮,不會加載<Java_Runtime_Home>/lib存在的陌生類,開發(fā)者通過將要加載的非JDK自身的類放置到此目錄下期待啟動類加載器加載是不可能的。做個進一步驗證,刪除<Java_Runtime_Home>/lib/ext目錄下和工程輸出目錄下的TestBean對應的class文件,然后再運行測試代碼,則將會有ClassNotFoundException異常拋出。有關(guān)這個問題,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中設(shè)置相應斷點運行測試三進行調(diào)試,會發(fā)現(xiàn)findBootstrapClass0()會拋出異常,然后在下面的findClass方法中被加載,當前運行的類加載器正是擴展類加載器(sun.misc.Launcher$ExtClassLoader),這一點可以通過JDT中變量視圖查看驗證。
3 java程序動態(tài)擴展方式
Java的連接模型允許用戶運行時擴展引用程序,既可以通過當前虛擬機中預定義的加載器加載編譯時已知的類或者接口,又允許用戶自行定義類裝載器,在運行時動態(tài)擴展用戶的程序。通過用戶自定義的類裝載器,你的程序可以裝載在編譯時并不知道或者尚未存在的類或者接口,并動態(tài)連接它們并進行有選擇的解析。
運行時動態(tài)擴展java應用程序有如下兩個途徑:
3.1 調(diào)用java.lang.Class.forName(…)加載類
這個方法其實在前面已經(jīng)討論過,在后面的問題2解答中說明了該方法調(diào)用會觸發(fā)哪個類加載器開始加載任務。這里需要說明的是多參數(shù)版本的forName(…)方法:
[java] view plaincopy
<embed id="ZeroClipboardMovie_12" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_12" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=12&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
- public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException
這里的initialize參數(shù)是很重要的。它表示在加載同時是否完成初始化的工作(說明:單參數(shù)版本的forName方法默認是完成初始化的)。有些場景下需要將initialize設(shè)置為true來強制加載同時完成初始化。例如典型的就是利用DriverManager進行JDBC驅(qū)動程序類注冊的問題。因為每一個JDBC驅(qū)動程序類的靜態(tài)初始化方法都用DriverManager注冊驅(qū)動程序,這樣才能被應用程序使用。這就要求驅(qū)動程序類必須被初始化,而不單單被加載。Class.forName的一個很常見的用法就是在加載數(shù)據(jù)庫驅(qū)動的時候。如 Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()用來加載 Apache Derby 數(shù)據(jù)庫的驅(qū)動。
3.2 用戶自定義類加載器
通過前面的分析,我們可以看出,除了和本地實現(xiàn)密切相關(guān)的啟動類加載器之外,包括標準擴展類加載器和系統(tǒng)類加載器在內(nèi)的所有其他類加載器我們都可以當做自定義類加載器來對待,唯一區(qū)別是是否被虛擬機默認使用。前面的內(nèi)容中已經(jīng)對java.lang.ClassLoader抽象類中的幾個重要的方法做了介紹,這里就簡要敘述一下一般用戶自定義類加載器的工作流程吧(可以結(jié)合后面問題解答一起看):
1、首先檢查請求的類型是否已經(jīng)被這個類裝載器裝載到命名空間中了,如果已經(jīng)裝載,直接返回;否則轉(zhuǎn)入步驟2;
2、委派類加載請求給父類加載器(更準確的說應該是雙親類加載器,真實虛擬機中各種類加載器最終會呈現(xiàn)樹狀結(jié)構(gòu)),如果父類加載器能夠完成,則返回父類加載器加載的Class實例;否則轉(zhuǎn)入步驟3;
3、調(diào)用本類加載器的findClass(…)方法,試圖獲取對應的字節(jié)碼,如果獲取的到,則調(diào)用defineClass(…)導入類型到方法區(qū);如果獲取不到對應的字節(jié)碼或者其他原因失敗,返回異常給loadClass(…), loadClass(…)轉(zhuǎn)而拋異常,終止加載過程(注意:這里的異常種類不止一種)。
說明:這里說的自定義類加載器是指JDK 1.2以后版本的寫法,即不覆寫改變java.lang.loadClass(…)已有委派邏輯情況下。
整個加載類的過程如下圖:
圖六 自定義類加載器加載類的過程
4 常見問題分析
4.1 由不同的類加載器加載的指定類還是相同的類型嗎?
在Java中,一個類用其完全匹配類名(fully qualified class name)作為標識,這里指的完全匹配類名包括包名和類名。但在JVM中一個類用其全名和一個加載類ClassLoader的實例作為唯一標識,不同類加載器加載的類將被置于不同的命名空間。我們可以用兩個自定義類加載器去加載某自定義類型(注意不要將自定義類型的字節(jié)碼放置到系統(tǒng)路徑或者擴展路徑中,否則會被系統(tǒng)類加載器或擴展類加載器搶先加載),然后用獲取到的兩個Class實例進行java.lang.Object.equals(…)判斷,將會得到不相等的結(jié)果。這個大家可以寫兩個自定義的類加載器去加載相同的自定義類型,然后做個判斷;同時,可以測試加載java.*類型,然后再對比測試一下測試結(jié)果。
4.2 在代碼中直接調(diào)用Class.forName(String name)方法,到底會觸發(fā)那個類加載器進行類加載行為?
Class.forName(String name)默認會使用調(diào)用類的類加載器來進行類加載。我們直接來分析一下對應的jdk的代碼:
[java] view plaincopy
<embed id="ZeroClipboardMovie_13" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_13" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=13&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
//java.lang.Class.java
publicstatic 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 it
if (caller == null) {
return null;
}
// 調(diào)用java.lang.Class中本地方法獲取加載該調(diào)用類(caller)的ClassLoader
return caller.getClassLoader0();
}
//java.lang.Class.java
//虛擬機本地實現(xiàn),獲取當前類的類加載器,前面介紹的Class的getClassLoader()也使用此方法
native ClassLoader getClassLoader0();
4.3 在編寫自定義類加載器時,如果沒有設(shè)定父加載器,那么父加載器是誰?
前面講過,在不指定父類加載器的情況下,默認采用系統(tǒng)類加載器。可能有人覺得不明白,現(xiàn)在我們來看一下JDK對應的代碼實現(xiàn)。眾所周知,我們編寫自定義的類加載器直接或者間接繼承自java.lang.ClassLoader抽象類,對應的無參默認構(gòu)造函數(shù)實現(xiàn)如下:
[java] view plaincopy
<embed id="ZeroClipboardMovie_14" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_14" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=14&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
- //摘自java.lang.ClassLoader.java
- protected ClassLoader() {
- SecurityManager security = System.getSecurityManager();
- if (security != null) {
- security.checkCreateClassLoader();
- }
- this.parent = getSystemClassLoader();
- initialized = true;
- }
我們再來看一下對應的getSystemClassLoader()方法的實現(xiàn):
[java] view plaincopy
<embed id="ZeroClipboardMovie_15" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_15" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=15&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
- private static synchronized void initSystemClassLoader() {
- //...
- sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
- scl = l.getClassLoader();
- //...
- }
我們可以寫簡單的測試代碼來測試一下:
[java] view plaincopy
<embed id="ZeroClipboardMovie_16" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_16" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=16&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
- System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());
本機對應輸出如下:
[plain] view plaincopy
<embed id="ZeroClipboardMovie_17" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_17" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=17&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
- sun.misc.Launcher$AppClassLoader@73d16e93
所以,我們現(xiàn)在可以相信當自定義類加載器沒有指定父類加載器的情況下,默認的父類加載器即為系統(tǒng)類加載器。同時,我們可以得出如下結(jié)論:即使用戶自定義類加載器不指定父類加載器,那么,同樣可以加載如下三個地方的類:
1. <Java_Runtime_Home>/lib下的類;
2. < Java_Runtime_Home >/lib/ext下或者由系統(tǒng)變量java.ext.dir指定位置中的類;
3. 當前工程類路徑下或者由系統(tǒng)變量java.class.path指定位置中的類。
4.4 在編寫自定義類加載器時,如果將父類加載器強制設(shè)置為null,那么會有什么影響?如果自定義的類加載器不能加載指定類,就肯定會加載失敗嗎?
JVM規(guī)范中規(guī)定如果用戶自定義的類加載器將父類加載器強制設(shè)置為null,那么會自動將啟動類加載器設(shè)置為當前用戶自定義類加載器的父類加載器(這個問題前面已經(jīng)分析過了)。同時,我們可以得出如下結(jié)論:
即使用戶自定義類加載器不指定父類加載器,那么,同樣可以加載到<Java_Runtime_Home>/lib下的類,但此時就不能夠加載<Java_Runtime_Home>/lib/ext目錄下的類了。
說明:問題3和問題4的推斷結(jié)論是基于用戶自定義的類加載器本身延續(xù)了java.lang.ClassLoader.loadClass(…)默認委派邏輯,如果用戶對這一默認委派邏輯進行了改變,以上推斷結(jié)論就不一定成立了,詳見問題5。
4.5 編寫自定義類加載器時,一般有哪些注意點?
1、一般盡量不要覆寫已有的loadClass(...)方法中的委派邏輯
一般在JDK 1.2之前的版本才這樣做,而且事實證明,這樣做極有可能引起系統(tǒng)默認的類加載器不能正常工作。在JVM規(guī)范和JDK文檔中(1.2或者以后版本中),都沒有建議用戶覆寫loadClass(…)方法,相比而言,明確提示開發(fā)者在開發(fā)自定義的類加載器時覆寫findClass(…)邏輯。舉一個例子來驗證該問題:
[java] view plaincopy
<embed id="ZeroClipboardMovie_18" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_18" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=18&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
//用戶自定義類加載器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下去加載類
// 具體實現(xiàn)代碼省略
}
}
通過前面的分析我們已經(jīng)知道,這個自定義類加載器WrongClassLoader的默認類加載器是系統(tǒng)類加載器,但是現(xiàn)在問題4種的結(jié)論就不成立了。大家可以簡單測試一下,現(xiàn)在<Java_Runtime_Home>/lib、< Java_Runtime_Home >/lib/ext和工程類路徑上的類都加載不上了。
[java] view plaincopy
<embed id="ZeroClipboardMovie_19" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_19" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=19&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
//問題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 plaincopy
<embed id="ZeroClipboardMovie_20" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_20" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=20&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
- 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.lang.Object都加載不到了。這里列舉的由于覆寫loadClass()引起的邏輯錯誤明顯是比較簡單的,實際引起的邏輯錯誤可能復雜的多。
[java] view plaincopy
<embed id="ZeroClipboardMovie_21" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_21" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=21&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
//問題5測試二
//用戶自定義類加載器WrongClassLoader.Java(不覆寫loadClass邏輯)
public class WrongClassLoader extends ClassLoader {
protected Class<?> findClass(String name) throws ClassNotFoundException {
//假設(shè)此處只是到工程以外的特定目錄D:\library下去加載類
//具體實現(xiàn)代碼省略
}
}
將自定義類加載器代碼WrongClassLoader.Java做以上修改后,再運行測試代碼,輸出結(jié)果如下:
[plain] view plaincopy
<embed id="ZeroClipboardMovie_22" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_22" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=22&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
- beans.Account
- WrongClassLoader@1c78e57
2、正確設(shè)置父類加載器
通過上面問題4和問題5的分析我們應該已經(jīng)理解,個人覺得這是自定義用戶類加載器時最重要的一點,但常常被忽略或者輕易帶過。有了前面JDK代碼的分析作為基礎(chǔ),我想現(xiàn)在大家都可以隨便舉出例子了。
3、保證findClass(String name)方法的邏輯正確性
事先盡量準確理解待定義的類加載器要完成的加載任務,確保最大程度上能夠獲取到對應的字節(jié)碼內(nèi)容。
4.6 如何在運行時判斷系統(tǒng)類加載器能加載哪些路徑下的類?
一是可以直接調(diào)用ClassLoader.getSystemClassLoader()或者其他方式獲取到系統(tǒng)類加載器(系統(tǒng)類加載器和擴展類加載器本身都派生自URLClassLoader),調(diào)用URLClassLoader中的getURLs()方法可以獲取到。
二是可以直接通過獲取系統(tǒng)屬性java.class.path來查看當前類路徑上的條目信息 :System.getProperty("java.class.path")。
4.7 如何在運行時判斷標準擴展類加載器能加載哪些路徑下的類?
方法之一:
[java] view plaincopy
<embed id="ZeroClipboardMovie_23" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_23" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=23&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
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) {
//…
}
}
}
本機對應輸出如下:
[plain] view plaincopy
<embed id="ZeroClipboardMovie_24" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_24" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=24&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
- 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來實現(xiàn)的;而啟動類的加載過程是通過調(diào)用loadClass來實現(xiàn)的。前者稱為一個類的定義加載器(defining loader),后者稱為初始加載器(initiating loader)。在Java虛擬機判斷兩個類是否相同的時候,使用的是類的定義加載器。也就是說,哪個類加載器啟動類的加載過程并不重要,重要的是最終定義這個類的加載器。兩種類加載器的關(guān)聯(lián)之處在于:一個類的定義加載器是它引用的其它類的初始加載器。如類 com.example.Outer引用了類 com.example.Inner,則由類 com.example.Outer的定義加載器負責啟動類 com.example.Inner的加載過程。
方法 loadClass()拋出的是 java.lang.ClassNotFoundException異常;方法 defineClass()拋出的是 java.lang.NoClassDefFoundError異常。
類加載器在成功加載某個類之后,會把得到的 java.lang.Class類的實例緩存起來。下次再請求加載該類的時候,類加載器會直接使用緩存的類的實例,而不會嘗試再次加載。也就是說,對于一個類加載器實例來說,相同全名的類只加載一次,即 loadClass方法不會被重復調(diào)用。
在絕大多數(shù)情況下,系統(tǒng)默認提供的類加載器實現(xiàn)已經(jīng)可以滿足需求。但是在某些情況下,您還是需要為應用開發(fā)出自己的類加載器。比如您的應用通過網(wǎng)絡(luò)來傳輸Java類的字節(jié)代碼,為了保證安全性,這些字節(jié)代碼經(jīng)過了加密處理。這個時候您就需要自己的類加載器來從某個網(wǎng)絡(luò)地址上讀取加密后的字節(jié)代碼,接著進行解密和驗證,最后定義出要在Java虛擬機中運行的類來。下面將通過兩個具體的實例來說明類加載器的開發(fā)。
5.1 文件系統(tǒng)類加載器
第一個類加載器用來加載存儲在文件系統(tǒng)上的Java字節(jié)代碼。完整的實現(xiàn)如下所示。
[java] view plaincopy
<embed id="ZeroClipboardMovie_25" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_25" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=25&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
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é)碼
@Override
protected 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()封裝了前面提到的代理模式的實現(xiàn)。該方法會首先調(diào)用findLoadedClass()方法來檢查該類是否已經(jīng)被加載過;如果沒有加載過的話,會調(diào)用父類加載器的loadClass()方法來嘗試加載該類;如果父類加載器無法加載該類的話,就調(diào)用findClass()方法來查找該類。因此,為了保證類加載器都正確實現(xiàn)代理模式,在開發(fā)自己的類加載器時,最好不要覆寫 loadClass()方法,而是覆寫 findClass()方法。
類 FileSystemClassLoader的 findClass()方法首先根據(jù)類的全名在硬盤上查找類的字節(jié)代碼文件(.class 文件),然后讀取該文件內(nèi)容,最后通過defineClass()方法來把這些字節(jié)代碼轉(zhuǎn)換成 java.lang.Class類的實例。
加載本地文件系統(tǒng)上的類,示例如下:
[java] view plaincopy
<embed id="ZeroClipboardMovie_26" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_26" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=26&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
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 plaincopy
<embed id="ZeroClipboardMovie_27" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_27" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=27&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
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();
}
}
}
運行輸出:com.example.Sample@7852e922
5.2 網(wǎng)絡(luò)類加載器
下面將通過一個網(wǎng)絡(luò)類加載器來說明如何通過類加載器來實現(xiàn)組件的動態(tài)更新。即基本的場景是:Java 字節(jié)代碼(.class)文件存放在服務器上,客戶端通過網(wǎng)絡(luò)的方式獲取字節(jié)代碼并執(zhí)行。當有版本更新的時候,只需要替換掉服務器上保存的文件即可。通過類加載器可以比較簡單的實現(xiàn)這種需求。
類 NetworkClassLoader負責通過網(wǎng)絡(luò)下載Java類字節(jié)代碼并定義出Java類。它的實現(xiàn)與FileSystemClassLoader類似。
[java] view plaincopy
<embed id="ZeroClipboardMovie_28" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_28" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=28&width=16&height=16" wmode="transparent" style="box-sizing: border-box;">
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) {
// 指定URL
this.rootUrl = rootUrl;
}
// 獲取類的字節(jié)碼
@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) {
// 從網(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) {
// 得到類文件的URL
return rootUrl + "/"
- className.replace('.', '/') + ".class";
}
}
在通過NetworkClassLoader加載了某個版本的類之后,一般有兩種做法來使用它。第一種做法是使用Java反射API。另外一種做法是使用接口。需要注意的是,并不能直接在客戶端代碼中引用從服務器上下載的類,因為客戶端代碼的類加載器找不到這些類。使用Java反射API可以直接調(diào)用Java類的方法。而使用接口的做法則是把接口的類放在客戶端中,從服務器上加載實現(xiàn)此接口的不同版本的類。在客戶端通過相同的接口來使用這些實現(xiàn)類。我們使用接口的方式。示例如下:
客戶端接口:
package classloader;
public interface Versioned {
String getVersion();
}
package classloader;
public interface ICalculator extends Versioned {
String calculate(String expression);
}
網(wǎng)絡(luò)上的不同版本的類:
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 CalculatorAdvanced implements ICalculator {
@Override
public String calculate(String expression) {
return "Result is " + expression;
}
@Override
public String getVersion() {
return "2.0";
}
}
在客戶端加載網(wǎng)絡(luò)上的類的過程:
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();
}
}
}