摘要:
每個開發人員對java.lang.ClassNotFoundExcetpion這個異常肯定都不陌生,這個異常背後涉及到的是Java技術體系中的類載入機制 機製。本文簡述了JVM三種預定義類載入器,即啓動類載入器、擴充套件類載入器和系統類載入器,並介紹和分析它們之間的關係和類載入所採用的雙親委派機制 機製,給出並分析了與Java類載入原理相關的若幹問題。
版權宣告:
本文作者:書呆子Rico
作者部落格地址:http://blog.csdn.net/justloveyou_/
一、引子
每個開發人員對java.lang.ClassNotFoundExcetpion這個異常肯定都不陌生,其實,這個異常背後涉及到的是Java技術體系中的類載入。Java類載入機制 機製雖然和大部分開發人員直接打交道的機會不多,但是對其機理的理解有助於排查程式出現的類載入失敗等技術問題,對理解Java虛擬機器的連線模型和Java語言的動態性都有很大幫助。
二. Java 虛擬機器類載入器結構簡述
1、JVM三種預定義型別類載入器
當JVM啓動的時候,Java開始使用如下三種類型的類載入器:
啓動(Bootstrap)類載入器:啓動類載入器是用原生代碼實現的類載入器,它負責將JAVA_HOME/lib下面 下麪的核心類庫或-Xbootclasspath選項指定的jar包等虛擬機器識別的類庫載入到記憶體中。由於啓動類載入器涉及到虛擬機器本地實現細節,開發者無法直接獲取到啓動類載入器的參照。具體可由啓動類載入器載入到的路徑可通過System.getProperty(「sun.boot.class.path」)檢視。
擴充套件(Extension)類載入器:擴充套件類載入器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的,它負責將JAVA_HOME /lib/ext或者由系統變數-Djava.ext.dir指定位置中的類庫載入到記憶體中。開發者可以直接使用標準擴充套件類載入器,具體可由擴充套件類載入器載入到的路徑可通過System.getProperty(「java.ext.dirs」)檢視。
系統(System)類載入器:系統類載入器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的,它負責將使用者類路徑(java -classpath或-Djava.class.path變數所指的目錄,即當前類所在路徑及其參照的第三方類庫的路徑,如第四節中的問題6所述)下的類庫載入到記憶體中。開發者可以直接使用系統類載入器,具體可由系統類載入器載入到的路徑可通過System.getProperty(「java.class.path」)檢視。
Ps: 除了以上列舉的三種類載入器,還有一種比較特殊的型別就是執行緒上下文類載入器,這個將在《深入理解Java類載入器(二):執行緒上下文類載入器》一文中進行單獨介紹。
2、類載入雙親委派機制 機製介紹和分析
JVM在載入類時預設採用的是雙親委派機制 機製。通俗的講,就是某個特定的類載入器在接到載入類的請求時,首先將載入任務委託給父類別載入器,依次遞回 (本質上就是loadClass函數的遞回呼叫),因此所有的載入請求最終都應該傳送到頂層的啓動類載入器中。如果父類別載入器可以完成這個類載入請求,就成功返回;只有當父類別載入器無法完成此載入請求時,子載入器纔會嘗試自己去載入。事實上,大多數情況下,越基礎的類由越上層的載入器進行載入,因爲這些基礎類之所以稱爲「基礎」,是因爲它們總是作爲被使用者程式碼呼叫的API(當然,也存在基礎類回撥使用者使用者程式碼的情形,即破壞雙親委派模型的情形)。 關於虛擬機器預設的雙親委派機制 機製,我們可以從系統類載入器和擴充套件類載入器爲例作簡單分析。
標準擴充套件類載入器繼承層次圖-17.2kB
系統類載入器繼承層次圖-16.4kB
上面兩張圖分別是擴充套件類載入器繼承層次圖和系統類載入器繼承層次圖。通過這兩張圖我們可以看出,擴充套件類載入器和系統類載入器均是繼承自 java.lang.ClassLoader抽象類。我們下面 下麪我們就看簡要介紹一下抽象類 java.lang.ClassLoader 中幾個最重要的方法:
//載入指定名稱(包括包名)的二進制型別,供使用者呼叫的介面
public Class<?> loadClass(String name) throws ClassNotFoundException{ … }
//載入指定名稱(包括包名)的二進制型別,同時指定是否解析(但是這裏的resolve參數不一定真正能達到解析的效果),供繼承用
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ … }
//findClass方法一般被loadClass方法呼叫去載入指定名稱類,供繼承用
protected Class<?> findClass(String name) throws ClassNotFoundException { … }
//定義型別,一般在findClass方法中讀取到對應位元組碼後呼叫,final的,不能被繼承
//這也從側面說明:JVM已經實現了對應的具體功能,解析對應的位元組碼,產生對應的內部數據結構放置到方法區,所以無需覆寫,直接呼叫就可以了)
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ … }
1
2
3
4
5
6
7
8
9
10
11
12
通過進一步分析標準擴充套件類載入器和系統類載入器的程式碼以及其公共父類別(java.net.URLClassLoader和java.security.SecureClassLoader)的程式碼可以看出,都沒有覆寫java.lang.ClassLoader中預設的載入委派規則 — loadClass(…)方法。既然這樣,我們就可以從java.lang.ClassLoader中的loadClass(String name)方法的程式碼中分析出虛擬機器預設採用的雙親委派機制 機製到底是什麼模樣:
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 { // 遞回終止條件
// 由於啓動類載入器無法被Java程式直接參照,因此預設用 null 替代
// parent == null就意味着由啓動類載入器嘗試載入該類,
// 即通過呼叫 native方法 findBootstrapClass0(String name)載入
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父類別載入器不能完成載入請求時,再呼叫自身的findClass方法進行類載入,若載入成功,findClass方法返回的是defineClass方法的返回值
// 注意,若自身也載入不了,會產生ClassNotFoundException異常並向上拋出
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
通過上面的程式碼分析,我們可以對JVM採用的雙親委派類載入機制 機製有了更直接的認識。下面 下麪我們就接着分析一下啓動類載入器、標準擴充套件類載入器和系統類載入器三者之間的關係。可能大家已經從各種資料上面看到瞭如下類似的一幅圖片:
類載入器預設委派關係圖-11.2kB
上面圖片給人的直觀印象是系統類載入器的父類別載入器是標準擴充套件類載入器,標準擴充套件類載入器的父類別載入器是啓動類載入器,下面 下麪我們就用程式碼具體測試一下:
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();
}
}
}/* Output:
sun.misc.LauncherExtClassLoader@70dea4e
null
*///:~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
通過以上的程式碼輸出,我們知道:通過java.lang.ClassLoader.getSystemClassLoader()可以直接獲取到系統類載入器 ,並且可以判定系統類載入器的父載入器是標準擴充套件類載入器,但是我們試圖獲取標準擴充套件類載入器的父類別載入器時卻得到了null。事實上,由於啓動類載入器無法被Java程式直接參照,因此JVM預設直接使用 null 代表啓動類載入器。我們還是藉助於程式碼分析一下,首先看一下java.lang.ClassLoader抽象類中預設實現的兩個建構函式:
protected ClassLoader() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
//預設將父類別載入器設定爲系統類載入器,getSystemClassLoader()獲取系統類載入器
this.parent = getSystemClassLoader();
initialized = true;
}
protected ClassLoader(ClassLoader parent) {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
//強制設定父類別載入器
this.parent = parent;
initialized = true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
緊接着,我們再看一下ClassLoader抽象類中parent成員的宣告:
// The parent class loader for delegation
private ClassLoader parent;
1
2
宣告爲私有變數的同時並沒有對外提供可供派生類存取的public或者protected設定器介面(對應的setter方法),結合前面的測試程式碼的輸出,我們可以推斷出:
1.系統類載入器(AppClassLoader)呼叫ClassLoader(ClassLoader parent)建構函式將父類別載入器設定爲標準擴充套件類載入器(ExtClassLoader)。(因爲如果不強制設定,預設會通過呼叫getSystemClassLoader()方法獲取並設定成系統類載入器,這顯然和測試輸出結果不符。)
2.擴充套件類載入器(ExtClassLoader)呼叫ClassLoader(ClassLoader parent)建構函式將父類別載入器設定爲null(null 本身就代表着引導類載入器)。(因爲如果不強制設定,預設會通過呼叫getSystemClassLoader()方法獲取並設定成系統類載入器,這顯然和測試輸出結果不符。)
事實上,這就是啓動類載入器、標準擴充套件類載入器和系統類載入器之間的委派關係。
3、類載入雙親委派範例
以上已經簡要介紹了虛擬機器預設使用的啓動類載入器、標準擴充套件類載入器和系統類載入器,並以三者爲例結合JDK程式碼對JVM預設使用的雙親委派類載入機制 機製做了分析。下面 下麪我們就來看一個綜合的例子,首先在IDE中建立一個簡單的java應用工程,然後寫一個簡單的JavaBean如下:
package classloader.test.bean;
public class TestBean {
public TestBean() { }
}
1
2
3
4
5
6
在現有當前工程中另外建立一個測試類(ClassLoaderTest.java)內容如下:
package classloader.test.bean;
public class ClassLoaderTest {
public static void main(String[] args) {
try {
//檢視當前系統類路徑中包含的路徑條目
System.out.println(System.getProperty("java.class.path"));
//呼叫載入當前類的類載入器(這裏即爲系統類載入器)載入TestBean
Class typeLoaded = Class.forName("classloader.test.bean.TestBean");
//檢視被載入的TestBean型別是被那個類載入器載入的
System.out.println(typeLoaded.getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}/* Output:
I:\AlgorithmPractice\TestClassLoader\bin
sun.misc.Launcher$AppClassLoader@6150818a
*///:~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
將當前工程輸出目錄下的TestBean.class打包進test.jar剪貼到<Java_Runtime_Home>/lib/ext目錄下(現在工程輸出目錄下和JRE擴充套件目錄下都有待載入型別的class檔案)。再執行測試一測試程式碼,結果如下:
I:\AlgorithmPractice\TestClassLoader\bin
sun.misc.Launcher$ExtClassLoader@15db9742
1
2
對比上面的兩個結果,我們明顯可以驗證前面說的雙親委派機制 機製:系統類載入器在接到載入classloader.test.bean.TestBean型別的請求時,首先將請求委派給父類別載入器(標準擴充套件類載入器),標準擴充套件類載入器搶先完成了載入請求。
最後,將test.jar拷貝一份到<Java_Runtime_Home>/lib下,執行測試程式碼,輸出如下:
I:\AlgorithmPractice\TestClassLoader\bin
sun.misc.Launcher$ExtClassLoader@15db9742
1
2
可以看到,後兩次輸出結果一致。那就是說,放置到<Java_Runtime_Home>/lib目錄下的TestBean對應的class位元組碼並沒有被載入,這其實和前面講的雙親委派機制 機製並不矛盾。虛擬機器出於安全等因素考慮,不會載入<JAVA_HOME>/lib目錄下存在的陌生類。換句話說,虛擬機器只載入<JAVA_HOME>/lib目錄下它可以識別的類。因此,開發者通過將要載入的非JDK自身的類放置到此目錄下期待啓動類載入器載入是不可能的。做個進一步驗證,刪除<JAVA_HOME>/lib/ext目錄下和工程輸出目錄下的TestBean對應的class檔案,然後再執行測試程式碼,則將會有ClassNotFoundException異常拋出。有關這個問題,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中設定相應斷點進行偵錯,會發現findBootstrapClass0()會拋出異常,然後在下面 下麪的findClass方法中被載入,當前執行的類載入器正是擴充套件類載入器(sun.misc.Launcher$ExtClassLoader),這一點可以通過JDT中變數檢視檢視驗證。
三. Java 程式動態擴充套件方式
Java的連線模型允許使用者執行時擴充套件參照程式,既可以通過當前虛擬機器中預定義的載入器載入編譯時已知的類或者介面,又允許使用者自行定義類裝載器,在執行時動態擴充套件使用者的程式。通過使用者自定義的類裝載器,你的程式可以載入在編譯時並不知道或者尚未存在的類或者介面,並動態連線它們並進行有選擇的解析。執行時動態擴充套件java應用程式有如下兩個途徑:
1、反射 (呼叫java.lang.Class.forName(…)載入類)
這個方法其實在前面已經討論過,在後面的問題2解答中說明了該方法呼叫會觸發哪個類載入器開始載入任務。這裏需要說明的是多參數版本的forName(…)方法:
public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException
1
這裏的initialize參數是很重要的,它表示在載入同時是否完成初始化的工作(說明:單參數版本的forName方法預設是完成初始化的)。有些場景下需要將initialize設定爲true來強制載入同時完成初始化,例如典型的就是載入數據庫驅動問題。因爲JDBC驅動程式只有被註冊後才能 纔能被應用程式使用,這就要求驅動程式類必須被初始化,而不單單被載入。
// 載入並範例化JDBC驅動類
Class.forName(driver);
// JDBC驅動類的實現
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
// 將initialize設定爲true來強制載入同時完成初始化,實現驅動註冊
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException(「Can’t register driver!」);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2、使用者自定義類載入器
通過前面的分析,我們可以看出,除了和本地實現密切相關的啓動類載入器之外,包括標準擴充套件類載入器和系統類載入器在內的所有其他類載入器我們都可以當做自定義類載入器來對待,唯一區別是是否被虛擬機器預設使用。前面的內容中已經對java.lang.ClassLoader抽象類中的幾個重要的方法做了介紹,這裏就簡要敘述一下一般使用者自定義類載入器的工作流程(可以結合後面問題解答一起看):
1、首先檢查請求的型別是否已經被這個類裝載器裝載到名稱空間中了,如果已經裝載,直接返回;否則轉入步驟2;
2、委派類載入請求給父類別載入器(更準確的說應該是雙親類載入器,真實虛擬機器中各種類載入器最終會呈現樹狀結構),如果父類別載入器能夠完成,則返回父類別載入器載入的Class範例;否則轉入步驟3;
3、呼叫本類載入器的findClass(…)方法,試圖獲取對應的位元組碼。如果獲取的到,則呼叫defineClass(…)匯入型別到方法區;如果獲取不到對應的位元組碼或者其他原因失敗, 向上拋異常給loadClass(…), loadClass(…)轉而呼叫findClass(…)方法處理異常,直至完成遞回呼叫。
必須指出的是,這裏所說的自定義類載入器是指JDK1.2以後版本的寫法,即不覆寫改變java.lang.loadClass(…)已有委派邏輯情況下。整個載入類的過程如下圖:
自定義類載入器載入類的過程-54.2kB
四. 常見問題分析
1、由不同的類載入器載入的指定類還是相同的型別嗎?
在Java中,一個類用其完全匹配類名(fully qualified class name)作爲標識,這裏指的完全匹配類名包括包名和類名。但在JVM中,一個類用其全名 和 一個ClassLoader的範例作爲唯一標識,不同類載入器載入的類將被置於不同的名稱空間。我們可以用兩個自定義類載入器去載入某自定義型別(注意不要將自定義型別的位元組碼放置到系統路徑或者擴充套件路徑中,否則會被系統類載入器或擴充套件類載入器搶先載入),然後用獲取到的兩個Class範例進行java.lang.Object.equals(…)判斷,將會得到不相等的結果,如下所示:
public class TestBean {
public static void main(String[] args) throws Exception {
// 一個簡單的類載入器,逆向雙親委派機制 機製
// 可以載入與自己在同一路徑下的Class檔案
ClassLoader myClassLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name)
throws ClassNotFoundException {
try {
String filename = name.substring(name.lastIndexOf(".") + 1)
+ ".class";
InputStream is = getClass().getResourceAsStream(filename);
if (is == null) {
return super.loadClass(name); // 遞回呼叫父類別載入器
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (Exception e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myClassLoader.loadClass("classloader.test.bean.TestBean")
.newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof classloader.test.bean.TestBean);
}
}/* Output:
class classloader.test.bean.TestBean
false
*///:~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
我們發現,obj 確實是類classloader.test.bean.TestBean範例化出來的物件,但當這個物件與類classloader.test.bean.TestBean做所屬型別檢查時卻返回了false。這是因爲虛擬機器中存在了兩個TestBean類,一個是由系統類載入器載入的,另一個則是由我們自定義的類載入器載入的,雖然它們來自同一個Class檔案,但依然是兩個獨立的類,因此做所屬型別檢查時返回false。
2、在程式碼中直接呼叫Class.forName(String name)方法,到底會觸發那個類載入器進行類載入行爲?
Class.forName(String name)預設會使用呼叫類的類載入器來進行類載入。我們直接來分析一下對應的jdk的程式碼:
//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() {
// 獲取呼叫類(caller)的型別
Class caller = Reflection.getCallerClass(3);
// This can be null if the VM is requesting it
if (caller == null) {
return null;
}
// 呼叫java.lang.Class中本地方法獲取載入該呼叫類(caller)的ClassLoader
return caller.getClassLoader0();
}
//java.lang.Class.java
//虛擬機器本地實現,獲取當前類的類載入器,前面介紹的Class的getClassLoader()也使用此方法
native ClassLoader getClassLoader0();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
3、在編寫自定義類載入器時,如果沒有設定父載入器,那麼父載入器是誰?
前面講過,在不指定父類別載入器的情況下,預設採用系統類載入器。可能有人覺得不明白,現在我們來看一下JDK對應的程式碼實現。衆所周知,我們編寫自定義的類載入器直接或者間接繼承自java.lang.ClassLoader抽象類,對應的無參預設建構函式實現如下:
//摘自java.lang.ClassLoader.java
protected ClassLoader() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
this.parent = getSystemClassLoader();
initialized = true;
}
1
2
3
4
5
6
7
8
9
我們再來看一下對應的getSystemClassLoader()方法的實現:
private static synchronized void initSystemClassLoader() {
//…
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
scl = l.getClassLoader();
//…
}
1
2
3
4
5
6
我們可以寫簡單的測試程式碼來測試一下:
System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());
1
本機對應輸出如下:
sun.misc.Launcher$AppClassLoader@73d16e93
1
所以,我們現在可以相信當自定義類載入器沒有指定父類別載入器的情況下,預設的父類別載入器即爲系統類載入器。同時,我們可以得出如下結論:即使使用者自定義類載入器不指定父類別載入器,那麼,同樣可以載入如下三個地方的類:
<Java_Runtime_Home>/lib下的類;
<Java_Runtime_Home>/lib/ext下或者由系統變數java.ext.dir指定位置中的類;
當前工程類路徑下或者由系統變數java.class.path指定位置中的類。
4、在編寫自定義類載入器時,如果將父類別載入器強制設定爲null,那麼會有什麼影響?如果自定義的類載入器不能載入指定類,就肯定會載入失敗嗎?
JVM規範中規定如果使用者自定義的類載入器將父類別載入器強制設定爲null,那麼會自動將啓動類載入器設定爲當前使用者自定義類載入器的父類別載入器(這個問題前面已經分析過了)。同時,我們可以得出如下結論:即使使用者自定義類載入器不指定父類別載入器,那麼,同樣可以載入到<JAVA_HOME>/lib下的類,但此時就不能夠載入<JAVA_HOME>/lib/ext目錄下的類了。
Ps:問題3和問題4的推斷結論是基於使用者自定義的類載入器本身延續了java.lang.ClassLoader.loadClass(…)預設委派邏輯,如果使用者對這一預設委派邏輯進行了改變,以上推斷結論就不一定成立了,詳見問題 5。
5、編寫自定義類載入器時,一般有哪些注意點?
1)、一般儘量不要覆寫已有的loadClass(…)方法中的委派邏輯(Old Generation)
一般在JDK 1.2之前的版本才這樣做,而且事實證明,這樣做極有可能引起系統預設的類載入器不能正常工作。在JVM規範和JDK文件中(1.2或者以後版本中),都沒有建議使用者覆寫loadClass(…)方法,相比而言,明確提示開發者在開發自定義的類載入器時覆寫findClass(…)邏輯。舉一個例子來驗證該問題:
//使用者自定義類載入器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 {
// 假設此處只是到工程以外的特定目錄D:\library下去載入類
// 具體實現程式碼省略
}
}
1
2
3
4
5
6
7
8
9
10
11
12
通過前面的分析我們已經知道,這個自定義類載入器WrongClassLoader的預設類載入器是系統類載入器,但是現在問題4中的結論就不成立了。大家可以簡單測試一下,現在<JAVA_HOME>/lib、<JAVA_HOME>/lib/ext 和 工程類路徑上的類都載入不上了。
//問題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();
}
}
}/* Output:
java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系統找不到指定的路徑。)
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.(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)
*///:~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
注意,這裏D:"classes"beans"Account.class是物理存在的。這說明,連要載入的型別的超型別java.lang.Object都載入不到了。這裏列舉的由於覆寫loadClass()引起的邏輯錯誤明顯是比較簡單的,實際引起的邏輯錯誤可能複雜的多。
//問題5測試二
//使用者自定義類載入器WrongClassLoader.Java(不覆寫loadClass邏輯)
public class WrongClassLoader extends ClassLoader {
protected Class<?> findClass(String name) throws ClassNotFoundException {
//假設此處只是到工程以外的特定目錄D:\library下去載入類
//具體實現程式碼省略
}
}/* Output:
beans.Account
WrongClassLoader@1c78e57
*///:~
1
2
3
4
5
6
7
8
9
10
11
將自定義類載入器程式碼WrongClassLoader.Java做以上修改後,再執行測試程式碼,輸出正確。
2). 正確設定父類別載入器
通過上面問題4和問題5的分析我們應該已經理解,個人覺得這是自定義使用者類載入器時最重要的一點,但常常被忽略或者輕易帶過。有了前面JDK程式碼的分析作爲基礎,我想現在大家都可以隨便舉出例子了。
3). 保證findClass(String name)方法的邏輯正確性
事先儘量準確理解待定義的類載入器要完成的載入任務,確保最大程度上能夠獲取到對應的位元組碼內容。
6、如何在執行時判斷系統類載入器能載入哪些路徑下的類?
一是可以直接呼叫ClassLoader.getSystemClassLoader()或者其他方式獲取到系統類載入器(系統類載入器和擴充套件類載入器本身都派生自URLClassLoader),呼叫URLClassLoader中的getURLs()方法可以獲取到。二是可以直接通過獲取系統屬性java.class.path來檢視當前類路徑上的條目資訊 :System.getProperty(「java.class.path」)。如下所示,
public class Test {
public static void main(String[] args) {
System.out.println(「Rico」);
Gson gson = new Gson();
System.out.println(gson.getClass().getClassLoader());
System.out.println(System.getProperty(「java.class.path」));
}
}/* Output:
Rico
sun.misc.Launcher$AppClassLoader@6c68bcef
I:\AlgorithmPractice\TestClassLoader\bin;I:\Java\jars\Gson\gson-2.3.1.jar
*///:~
1
2
3
4
5
6
7
8
9
10
11
12
如上述程式所示,Test類和Gson類由系統類載入器載入,並且其載入路徑就是使用者類路徑,包括當前類路徑和參照的第三方類庫的路徑。
7、如何在執行時判斷標準擴充套件類載入器能載入哪些路徑下的類?
利用如下方式即可判斷:
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) {
//…
}
}
} /* Output:
file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/access-bridge-64.jar
file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/dnsns.jar
file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/jaccess.jar
file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/localedata.jar
file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/sunec.jar
file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/sunjce_provider.jar
file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/sunmscapi.jar
file:/C:/Program%20Files/Java/jdk1.7.0_79/jre/lib/ext/zipfs.jar
*///:~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
五. 開發自己的類載入器
在前面介紹類載入器的代理委派模型的時候,提到過類載入器會首先代理給其它類載入器來嘗試載入某個類,這就意味着真正完成類的載入工作的類載入器和啓動這個載入過程的類載入器,有可能不是同一個。真正完成類的載入工作是通過呼叫defineClass來實現的;而啓動類的載入過程是通過呼叫loadClass來實現的。前者稱爲一個類的定義載入器(defining loader),後者稱爲初始載入器(initiating loader)。在Java虛擬機器判斷兩個類是否相同的時候,使用的是類的定義載入器。也就是說,哪個類載入器啓動類的載入過程並不重要,重要的是最終定義這個類的載入器。兩種類載入器的關聯之處在於:一個類的定義載入器是它參照的其它類的初始載入器。如類 com.example.Outer參照了類 com.example.Inner,則由類 com.example.Outer的定義載入器負責啓動類 com.example.Inner的載入過程。
方法 loadClass()拋出的是 java.lang.ClassNotFoundException異常;方法 defineClass()拋出的是 java.lang.NoClassDefFoundError異常。
類載入器在成功載入某個類之後,會把得到的 java.lang.Class類的範例快取起來。下次再請求載入該類的時候,類載入器會直接使用快取的類的範例,而不會嘗試再次載入。也就是說,對於一個類載入器範例來說,相同全名的類只載入一次,即 loadClass方法不會被重複呼叫。
在絕大多數情況下,系統預設提供的類載入器實現已經可以滿足需求。但是在某些情況下,您還是需要爲應用開發出自己的類載入器。比如您的應用通過網路來傳輸Java類的位元組程式碼,爲了保證安全性,這些位元組程式碼經過了加密處理。這個時候您就需要自己的類載入器來從某個網路地址上讀取加密後的位元組程式碼,接着進行解密和驗證,最後定義出要在Java虛擬機器中執行的類來。下面 下麪將通過兩個具體的範例來說明類載入器的開發。
1、檔案系統類載入器
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";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
如上所示,類 FileSystemClassLoader繼承自類java.lang.ClassLoader。在java.lang.ClassLoader類的常用方法中,一般來說,自己開發的類載入器只需要覆寫 findClass(String name)方法即可。java.lang.ClassLoader類的方法loadClass()封裝了前面提到的代理模式的實現。該方法會首先呼叫findLoadedClass()方法來檢查該類是否已經被載入過;如果沒有載入過的話,會呼叫父類別載入器的loadClass()方法來嘗試載入該類;如果父類別載入器無法載入該類的話,就呼叫findClass()方法來查詢該類。因此,爲了保證類載入器都正確實現代理模式,在開發自己的類載入器時,最好不要覆寫 loadClass()方法,而是覆寫 findClass()方法。
類 FileSystemClassLoader的 findClass()方法首先根據類的全名在硬碟上查詢類的位元組程式碼檔案(.class 檔案),然後讀取該檔案內容,最後通過defineClass()方法來把這些位元組程式碼轉換成 java.lang.Class類的範例。載入本地檔案系統上的類,範例如下:
package com.example;
public class Sample {
private Sample instance;
public void setSample(Object instance) {
System.out.println(instance.toString());
this.instance = (Sample) instance;
}
}
1
2
3
4
5
6
7
8
9
10
11
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(); // 建立物件
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();
}
}
}/* Output:
com.example.Sample@7852e922
*///:~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2、網路類載入器
下面 下麪將通過一個網路類載入器來說明如何通過類載入器來實現元件的動態更新。即基本的場景是:Java 位元組程式碼(.class)檔案存放在伺服器上,用戶端通過網路的方式獲取位元組程式碼並執行。當有版本更新的時候,只需要替換掉伺服器上儲存的檔案即可。通過類載入器可以比較簡單的實現這種需求。
類 NetworkClassLoader負責通過網路下載Java類位元組程式碼並定義出Java類。它的實現與FileSystemClassLoader類似。
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;
}
// 獲取類的位元組碼
@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) {
// 得到類檔案的URL
return rootUrl + "/"
+ className.replace('.', '/') + ".class";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
在通過NetworkClassLoader載入了某個版本的類之後,一般有兩種做法來使用它。第一種做法是使用Java反射API。另外一種做法是使用介面。需要注意的是,並不能直接在用戶端程式碼中參照從伺服器上下載的類,因爲用戶端程式碼的類載入器找不到這些類。使用Java反射API可以直接呼叫Java類的方法。而使用介面的做法則是把介面的類放在用戶端中,從伺服器上載入實現此介面的不同版本的類。在用戶端通過相同的介面來使用這些實現類。我們使用介面的方式。範例如下:
用戶端介面:
package classloader;
public interface Versioned {
String getVersion();
}
1
2
3
4
5
6
package classloader;
public interface ICalculator extends Versioned {
String calculate(String expression);
}
1
2
3
4
5
6
網路上的不同版本的類:
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";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在用戶端載入網路上的類的過程:
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();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
六. 更多
雙親委派模型是Java推薦的類載入模型,但違背該模型的案例有哪些?爲什麼會違背,又是怎麼解決這種case的?這個將在《雙親委派模型與執行緒上下文類載入器》一文中進行介紹。