Java類載入原理解析

不放棄的泉發表於2017-10-06

1 基本資訊


  每個開發人員對java.lang.ClassNotFoundExcetpion這個異常肯定都不陌生,這背後就涉及到了java技術體系中的類載入。Java的類載入機制是技術體系中比較核心的部分,雖然和大部分開發人員直接打交道不多,但是對其背後的機理有一定理解有助於排查程式中出現的類載入失敗等技術問題,對理解java虛擬機器的連線模型和java語言的動態性都有很大幫助。


2 Java虛擬機器類載入器結構簡述


2.1 JVM三種預定義型別類載入器


  我們首先看一下JVM預定義的三種型別類載入器,當一個 JVM啟動的時候,Java預設開始使用如下三種型別類裝入器:

  啟動(Bootstrap)類載入器:引導類裝入器是用原生程式碼實現的類裝入器,它負責將 <Java_Runtime_Home>/lib下面的核心類庫或-Xbootclasspath選項指定的jar包載入到記憶體中。由於引導類載入器涉及到虛擬機器本地實現細節,開發者無法直接獲取到啟動類載入器的引用,所以不允許直接通過引用進行操作。

  擴充套件(Extension)類載入器:擴充套件類載入器是由SunExtClassLoadersun.misc.Launcher$ExtClassLoader實現的。它負責將< Java_Runtime_Home >/lib/ext或者由系統變數-Djava.ext.dir指定位置中的類庫載入到記憶體中。開發者可以直接使用標準擴充套件類載入器。

  系統(System)類載入器:系統類載入器是由 Sun AppClassLoadersun.misc.Launcher$AppClassLoader)實現的。它負責將系統類路徑java -classpath或-Djava.class.path變數所指的目錄下的類庫載入到記憶體中。開發者可以直接使用系統類載入器。

  除了以上列舉的三種類載入器,還有一種比較特殊的型別就是執行緒上下文類載入器,這個將在後面單獨介紹。


2.類載入雙親委派機制介紹和分析


       在這裡,需要著重說明的是,JVM在載入類時預設採用的是雙親委派機制。通俗的講,就是某個特定的類載入器在接到載入類的請求時,首先將載入任務委託給父類載入器,依次遞迴,如果父類載入器可以完成類載入任務,就成功返回;只有父類載入器無法完成此載入任務時,才自己去載入。關於虛擬機器預設的雙親委派機制,我們可以從系統類載入器和擴充套件類載入器為例作簡單分析。

圖一 標準擴充套件類載入器繼承層次圖

圖二系統類載入器繼承層次圖

通過圖一和圖二我們可以看出,類載入器均是繼承自java.lang.ClassLoader抽象類。我們下面我們就看簡要介紹一下java.lang.ClassLoader中幾個最重要的方法

[java] view plain copy
  1. //載入指定名稱(包括包名)的二進位制型別,供使用者呼叫的介面  
  2. public Class<?> loadClass(String name) throws ClassNotFoundException{ … }  
  3.   
  4. //載入指定名稱(包括包名)的二進位制型別,同時指定是否解析(但是這裡的resolve引數不一定真正能達到解析的效果),供繼承用  
  5. protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ … }  
  6.   
  7. //findClass方法一般被loadClass方法呼叫去載入指定名稱類,供繼承用  
  8. protected Class<?> findClass(String name) throws ClassNotFoundException { … }  
  9.   
  10. //定義型別,一般在findClass方法中讀取到對應位元組碼後呼叫,可以看出不可繼承  
  11. //(說明:JVM已經實現了對應的具體功能,解析對應的位元組碼,產生對應的內部資料結構放置到方法區,所以無需覆寫,直接呼叫就可以了)  
  12. protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ … }  
  通過進一步分析標準擴充套件類載入器(sun.misc.Launcher$ExtClassLoader)和系統類載入器(sun.misc.Launcher$AppClassLoader)的程式碼以及其公共父類(java.net.URLClassLoaderjava.security.SecureClassLoader)的程式碼可以看出,都沒有覆寫java.lang.ClassLoader中預設的載入委派規則---loadClass)方法。既然這樣,我們就可以通過分析java.lang.ClassLoader中的loadClassString name)方法的程式碼就可以分析出虛擬機器預設採用的雙親委派機制到底是什麼模樣:
[java] view plain copy
  1. public Class<?> loadClass(String name) throws ClassNotFoundException {  
  2.     return loadClass(name, false);  
  3. }  
  4.   
  5. protected synchronized Class<?> loadClass(String name, boolean resolve)  
  6.         throws ClassNotFoundException {  
  7.   
  8.     // 首先判斷該型別是否已經被載入  
  9.     Class c = findLoadedClass(name);  
  10.     if (c == null) {  
  11.         //如果沒有被載入,就委託給父類載入或者委派給啟動類載入器載入  
  12.         try {  
  13.             if (parent != null) {  
  14.                 //如果存在父類載入器,就委派給父類載入器載入  
  15.                 c = parent.loadClass(name, false);  
  16.             } else {  
  17.                 //如果不存在父類載入器,就檢查是否是由啟動類載入器載入的類,  
  18.                 //通過呼叫本地方法native findBootstrapClass0(String name)  
  19.                 c = findBootstrapClass0(name);  
  20.             }  
  21.         } catch (ClassNotFoundException e) {  
  22.             // 如果父類載入器和啟動類載入器都不能完成載入任務,才呼叫自身的載入功能  
  23.             c = findClass(name);  
  24.         }  
  25.     }  
  26.     if (resolve) {  
  27.         resolveClass(c);  
  28.     }  
  29.     return c;  
  30. }  
  通過上面的程式碼分析,我們可以對JVM採用的雙親委派類載入機制有了更感性的認識,下面我們就接著分析一下啟動類載入器、標準擴充套件類載入器和系統類載入器三者之間的關係。可能大家已經從各種資料上面看到了如下類似的一幅圖片:

圖三 類載入器預設委派關係圖

  上面圖片給人的直觀印象是系統類載入器的父類載入器是標準擴充套件類載入器,標準擴充套件類載入器的父類載入器是啟動類載入器,下面我們就用程式碼具體測試一下:

[java] view plain copy
  1. public class LoaderTest {  
  2.   
  3.     public static void main(String[] args) {  
  4.         try {  
  5.             System.out.println(ClassLoader.getSystemClassLoader());  
  6.             System.out.println(ClassLoader.getSystemClassLoader().getParent());  
  7.             System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());  
  8.         } catch (Exception e) {  
  9.             e.printStackTrace();  
  10.         }  
  11.     }  
  12. }  
  說明:通過java.lang.ClassLoader.getSystemClassLoader()可以直接獲取到系統類載入器。
  程式碼輸出如下:
[plain] view plain copy
  1. sun.misc.Launcher$AppClassLoader@6d06d69c  
  2. sun.misc.Launcher$ExtClassLoader@70dea4e  
  3. null  
  通過以上的程式碼輸出,我們可以判定系統類載入器的父載入器是標準擴充套件類載入器,但是我們試圖獲取標準擴充套件類載入器的父類載入器時確得到了null,就是說標準擴充套件類載入器本身強制設定父類載入器為null我們還是藉助於程式碼分析一下。

  我們首先看一下java.lang.ClassLoader抽象類中預設實現的兩個建構函式:

[java] view plain copy
  1. protected ClassLoader() {  
  2.     SecurityManager security = System.getSecurityManager();  
  3.     if (security != null) {  
  4.         security.checkCreateClassLoader();  
  5.     }  
  6.     //預設將父類載入器設定為系統類載入器,getSystemClassLoader()獲取系統類載入器  
  7.     this.parent = getSystemClassLoader();  
  8.     initialized = true;  
  9. }  
  10.   
  11. protected ClassLoader(ClassLoader parent) {  
  12.     SecurityManager security = System.getSecurityManager();  
  13.     if (security != null) {  
  14.         security.checkCreateClassLoader();  
  15.     }  
  16.     //強制設定父類載入器  
  17.     this.parent = parent;  
  18.     initialized = true;  
  19. }  
  我們再看一下ClassLoader抽象類中parent成員的宣告:
[java] view plain copy
  1. // The parent class loader for delegation  
  2. private ClassLoader parent;  

  宣告為私有變數的同時並沒有對外提供可供派生類訪問的public或者protected設定器介面(對應的setter方法),結合前面的測試程式碼的輸出,我們可以推斷出:
  1. 系統類載入器(AppClassLoader)呼叫ClassLoader(ClassLoader parent)建構函式將父類載入器設定為標準擴充套件類載入器(ExtClassLoader)。(因為如果不強制設定,預設會通過呼叫getSystemClassLoader()方法獲取並設定成系統類載入器,這顯然和測試輸出結果不符。)
  2. 擴充套件類載入器(ExtClassLoader)呼叫ClassLoader(ClassLoader parent)建構函式將父類載入器設定為null。(因為如果不強制設定,預設會通過呼叫getSystemClassLoader()方法獲取並設定成系統類載入器,這顯然和測試輸出結果不符。)
  現在我們可能會有這樣的疑問:擴充套件類載入器(ExtClassLoader)的父類載入器被強制設定為null了,那麼擴充套件類載入器為什麼還能將載入任務委派給啟動類載入器呢?

圖四 標準擴充套件類載入器和系統類載入器成員大綱檢視


圖五 擴充套件類載入器和系統類載入器公共父類成員大綱檢視

  通過圖四和圖五可以看出,標準擴充套件類載入器和系統類載入器及其父類(java.net.URLClassLoader和java.security.SecureClassLoader)都沒有覆寫java.lang.ClassLoader中預設的載入委派規則---loadClass(…)方法。有關java.lang.ClassLoader中預設的載入委派規則前面已經分析過,如果父載入器為null,則會呼叫本地方法進行啟動類載入嘗試。所以,圖三中,啟動類載入器、標準擴充套件類載入器和系統類載入器之間的委派關係事實上是仍就成立的。(在後面的使用者自定義類載入器部分,還會做更深入的分析)。


2.3 類載入雙親委派示例


  以上已經簡要介紹了虛擬機器預設使用的啟動類載入器、標準擴充套件類載入器和系統類載入器,並以三者為例結合JDK程式碼對JVM預設使用的雙親委派類載入機制做了分析。下面我們就來看一個綜合的例子。首先在IDE中建立一個簡單的java應用工程,然後寫一個簡單的JavaBean如下:

[java] view plain copy
  1. package classloader.test.bean;  
  2.   
  3. public class TestBean {  
  4.       
  5.     public TestBean() { }  
  6. }  
  在現有當前工程中另外建立一測試類(ClassLoaderTest.java)內容如下:

  測試一:

[java] view plain copy
  1. package classloader.test.bean;  
  2.   
  3. public class ClassLoaderTest {  
  4.   
  5.     public static void main(String[] args) {  
  6.         try {  
  7.             //檢視當前系統類路徑中包含的路徑條目  
  8.             System.out.println(System.getProperty("java.class.path"));  
  9.             //呼叫載入當前類的類載入器(這裡即為系統類載入器)載入TestBean  
  10.             Class typeLoaded = Class.forName("classloader.test.bean.TestBean");  
  11.             //檢視被載入的TestBean型別是被那個類載入器載入的  
  12.             System.out.println(typeLoaded.getClassLoader());  
  13.         } catch (Exception e) {  
  14.             e.printStackTrace();  
  15.         }  
  16.     }  
  17. }  
  對應的輸出如下:

[plain] view plain copy
  1. C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes  
  2. sun.misc.Launcher$AppClassLoader@73d16e93  
  說明:當前類路徑預設的含有的一個條目就是工程的輸出目錄。
  測試二:

  將當前工程輸出目錄下的TestBean.class打包進test.jar剪貼<Java_Runtime_Home>/lib/ext目錄下(現在工程輸出目錄下和JRE擴充套件目錄下都有待載入型別的class檔案)。再執行測試一測試程式碼,結果如下:

[plain] view plain copy
  1. C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes  
  2. sun.misc.Launcher$ExtClassLoader@15db9742  
  對比測試一和測試二,我們明顯可以驗證前面說的雙親委派機制,系統類載入器在接到載入classloader.test.bean.TestBean型別的請求時,首先將請求委派給父類載入器(標準擴充套件類載入器),標準擴充套件類載入器搶先完成了載入請求。
  測試三:
  將test.jar拷貝一份到<Java_Runtime_Home>/lib下,執行測試程式碼,輸出如下:
[plain] view plain copy
  1. C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes  
  2. sun.misc.Launcher$ExtClassLoader@15db9742  
  測試三和測試二輸出結果一致。那就是說,放置到<Java_Runtime_Home>/lib目錄下的TestBean對應的class位元組碼並沒有被載入,這其實和前面講的雙親委派機制並不矛盾。虛擬機器出於安全等因素考慮,不會載入<Java_Runtime_Home>/lib存在的陌生類,開發者通過將要載入的非JDK自身的類放置到此目錄下期待啟動類載入器載入是不可能的。做個進一步驗證,刪除<Java_Runtime_Home>/lib/ext目錄下和工程輸出目錄下的TestBean對應的class檔案,然後再執行測試程式碼,則將會有ClassNotFoundException異常丟擲。有關這個問題,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中設定相應斷點執行測試三進行除錯,會發現findBootstrapClass0()會丟擲異常,然後在下面的findClass方法中被載入,當前執行的類載入器正是擴充套件類載入器(sun.misc.Launcher$ExtClassLoader),這一點可以通過JDT中變數檢視檢視驗證。


3 java程式動態擴充套件方式


  Java的連線模型允許使用者執行時擴充套件引用程式,既可以通過當前虛擬機器中預定義的載入器載入編譯時已知的類或者介面,又允許使用者自行定義類裝載器,在執行時動態擴充套件使用者的程式。通過使用者自定義的類裝載器,你的程式可以裝載在編譯時並不知道或者尚未存在的類或者介面,並動態連線它們並進行有選擇的解析。
  執行時動態擴充套件java應用程式有如下兩個途徑:


3.1 呼叫java.lang.Class.forName(…)載入類


  這個方法其實在前面已經討論過,在後面的問題2解答中說明了該方法呼叫會觸發哪個類載入器開始載入任務。這裡需要說明的是多引數版本的forName(…)方法:

[java] view plain copy
  1. public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException  
  這裡的initialize引數是很重要的。它表示在載入同時是否完成初始化的工作(說明:單引數版本的forName方法預設是完成初始化的)。有些場景下需要將initialize設定為true來強制載入同時完成初始化。例如典型的就是利用DriverManager進行JDBC驅動程式類註冊的問題。因為每一個JDBC驅動程式類的靜態初始化方法都用DriverManager註冊驅動程式,這樣才能被應用程式使用。這就要求驅動程式類必須被初始化,而不單單被載入。Class.forName的一個很常見的用法就是在載入資料庫驅動的時候。如 Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()用來載入 Apache Derby 資料庫的驅動。


3.2 使用者自定義類載入器


  通過前面的分析,我們可以看出,除了和本地實現密切相關的啟動類載入器之外,包括標準擴充套件類載入器和系統類載入器在內的所有其他類載入器我們都可以當做自定義類載入器來對待,唯一區別是是否被虛擬機器預設使用。前面的內容中已經對java.lang.ClassLoader抽象類中的幾個重要的方法做了介紹,這裡就簡要敘述一下一般使用者自定義類載入器的工作流程吧(可以結合後面問題解答一起看):
  1、首先檢查請求的型別是否已經被這個類裝載器裝載到名稱空間中了,如果已經裝載,直接返回;否則轉入步驟2;
  2、委派類載入請求給父類載入器(更準確的說應該是雙親類載入器,真實虛擬機器中各種類載入器最終會呈現樹狀結構),如果父類載入器能夠完成,則返回父類載入器載入的Class例項;否則轉入步驟3;
  3、呼叫本類載入器的findClass(…)方法,試圖獲取對應的位元組碼,如果獲取的到,則呼叫defineClass(…)匯入型別到方法區;如果獲取不到對應的位元組碼或者其他原因失敗,返回異常給loadClass(…), loadClass(…)轉而拋異常,終止載入過程(注意:這裡的異常種類不止一種)。
  說明:這裡說的自定義類載入器是指JDK 1.2以後版本的寫法,即不覆寫改變java.lang.loadClass(…)已有委派邏輯情況下。

  整個載入類的過程如下圖:

圖六 自定義類載入器載入類的過程


4 常見問題分析


4.1 由不同的類載入器載入的指定類還是相同的型別嗎?


  在Java中,一個類用其完全匹配類名(fully qualified class name)作為標識,這裡指的完全匹配類名包括包名和類名。但在JVM中一個類用其全名和一個載入類ClassLoader的例項作為唯一標識,不同類載入器載入的類將被置於不同的名稱空間。我們可以用兩個自定義類載入器去載入某自定義型別(注意不要將自定義型別的位元組碼放置到系統路徑或者擴充套件路徑中,否則會被系統類載入器或擴充套件類載入器搶先載入),然後用獲取到的兩個Class例項進行java.lang.Object.equals(…)判斷,將會得到不相等的結果。這個大家可以寫兩個自定義的類載入器去載入相同的自定義型別,然後做個判斷;同時,可以測試載入java.*型別,然後再對比測試一下測試結果。


4.2 在程式碼中直接呼叫Class.forName(String name)方法,到底會觸發那個類載入器進行類載入行為?


  Class.forName(String name)預設會使用呼叫類的類載入器來進行類載入。我們直接來分析一下對應的jdk的程式碼:

[java] view plain copy
  1. //java.lang.Class.java  
  2. publicstatic Class<?> forName(String className) throws ClassNotFoundException {  
  3.     return forName0(className, true, ClassLoader.getCallerClassLoader());  
  4. }  
  5.   
  6. //java.lang.ClassLoader.java  
  7. // Returns the invoker's class loader, or null if none.  
  8. static ClassLoader getCallerClassLoader() {  
  9.     // 獲取呼叫類(caller)的型別  
  10.     Class caller = Reflection.getCallerClass(3);  
  11.     // This can be null if the VM is requesting it  
  12.     if (caller == null) {  
  13.         return null;  
  14.     }  
  15.     // 呼叫java.lang.Class中本地方法獲取載入該呼叫類(caller)的ClassLoader  
  16.     return caller.getClassLoader0();  
  17. }  
  18.   
  19. //java.lang.Class.java  
  20. //虛擬機器本地實現,獲取當前類的類載入器,前面介紹的Class的getClassLoader()也使用此方法  
  21. native ClassLoader getClassLoader0();  


4.3 在編寫自定義類載入器時,如果沒有設定父載入器,那麼父載入器是誰?


  前面講過,在不指定父類載入器的情況下,預設採用系統類載入器。可能有人覺得不明白,現在我們來看一下JDK對應的程式碼實現。眾所周知,我們編寫自定義的類載入器直接或者間接繼承自java.lang.ClassLoader抽象類,對應的無參預設建構函式實現如下:

[java] view plain copy
  1. //摘自java.lang.ClassLoader.java  
  2. protected ClassLoader() {  
  3.     SecurityManager security = System.getSecurityManager();  
  4.     if (security != null) {  
  5.         security.checkCreateClassLoader();  
  6.     }  
  7.     this.parent = getSystemClassLoader();  
  8.     initialized = true;  
  9. }  
  我們再來看一下對應的getSystemClassLoader()方法的實現:
[java] view plain copy
  1. private static synchronized void initSystemClassLoader() {  
  2.     //...  
  3.     sun.misc.Launcher l = sun.misc.Launcher.getLauncher();  
  4.     scl = l.getClassLoader();  
  5.     //...  
  6. }  
  我們可以寫簡單的測試程式碼來測試一下:
[java] view plain copy
  1. System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());  
  本機對應輸出如下:
[plain] view plain copy
  1. sun.misc.Launcher$AppClassLoader@73d16e93  
  所以,我們現在可以相信當自定義類載入器沒有指定父類載入器的情況下,預設的父類載入器即為系統類載入器。同時,我們可以得出如下結論:即使使用者自定義類載入器不指定父類載入器,那麼,同樣可以載入如下三個地方的類:
  1. <Java_Runtime_Home>/lib下的類;
  2. < Java_Runtime_Home >/lib/ext下或者由系統變數java.ext.dir指定位置中的類;
  3. 當前工程類路徑下或者由系統變數java.class.path指定位置中的類。


4.4 在編寫自定義類載入器時,如果將父類載入器強制設定為null,那麼會有什麼影響?如果自定義的類載入器不能載入指定類,就肯定會載入失敗嗎?


  JVM規範中規定如果使用者自定義的類載入器將父類載入器強制設定為null,那麼會自動將啟動類載入器設定為當前使用者自定義類載入器的父類載入器(這個問題前面已經分析過了)。同時,我們可以得出如下結論:
  即使使用者自定義類載入器不指定父類載入器,那麼,同樣可以載入到<Java_Runtime_Home>/lib下的類,但此時就不能夠載入<Java_Runtime_Home>/lib/ext目錄下的類了。
  說明:問題3和問題4的推斷結論是基於使用者自定義的類載入器本身延續了java.lang.ClassLoader.loadClass(…)預設委派邏輯,如果使用者對這一預設委派邏輯進行了改變,以上推斷結論就不一定成立了,詳見問題5。


4.5 編寫自定義類載入器時,一般有哪些注意點?


  1、一般儘量不要覆寫已有的loadClass(...)方法中的委派邏輯
  一般在JDK 1.2之前的版本才這樣做,而且事實證明,這樣做極有可能引起系統預設的類載入器不能正常工作。在JVM規範和JDK文件中(1.2或者以後版本中),都沒有建議使用者覆寫loadClass(…)方法,相比而言,明確提示開發者在開發自定義的類載入器時覆寫findClass(…)邏輯。舉一個例子來驗證該問題:

[java] view plain copy
  1. //使用者自定義類載入器WrongClassLoader.Java(覆寫loadClass邏輯)  
  2. public class WrongClassLoader extends ClassLoader {  
  3.   
  4.     public Class<?> loadClass(String name) throws ClassNotFoundException {  
  5.         return this.findClass(name);  
  6.     }  
  7.   
  8.     protected Class<?> findClass(String name) throws ClassNotFoundException {  
  9.         // 假設此處只是到工程以外的特定目錄D:\library下去載入類  
  10.         // 具體實現程式碼省略  
  11.     }  
  12. }  
  通過前面的分析我們已經知道,這個自定義類載入器WrongClassLoader的預設類載入器是系統類載入器,但是現在問題4種的結論就不成立了。大家可以簡單測試一下,現在<Java_Runtime_Home>/lib、< Java_Runtime_Home >/lib/ext和工程類路徑上的類都載入不上了。
[java] view plain copy
  1. //問題5測試程式碼一  
  2. public class WrongClassLoaderTest {  
  3.   
  4.     publicstaticvoid main(String[] args) {  
  5.         try {  
  6.             WrongClassLoader loader = new WrongClassLoader();  
  7.             Class classLoaded = loader.loadClass("beans.Account");  
  8.             System.out.println(classLoaded.getName());  
  9.             System.out.println(classLoaded.getClassLoader());  
  10.         } catch (Exception e) {  
  11.             e.printStackTrace();  
  12.         }  
  13.     }  
  14. }  
  這裡D:"classes"beans"Account.class是物理存在的。輸出結果:
[plain] view plain copy
  1. java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系統找不到指定的路徑。)  
  2.     at java.io.FileInputStream.open(Native Method)  
  3.     at java.io.FileInputStream.<init>(FileInputStream.java:106)  
  4.     at WrongClassLoader.findClass(WrongClassLoader.java:40)  
  5.     at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
  6.     at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)  
  7.     at java.lang.ClassLoader.defineClass1(Native Method)  
  8.     at java.lang.ClassLoader.defineClass(ClassLoader.java:620)  
  9.     at java.lang.ClassLoader.defineClass(ClassLoader.java:400)  
  10.     at WrongClassLoader.findClass(WrongClassLoader.java:43)  
  11.     at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
  12.     at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)  
  13. Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object  
  14.     at java.lang.ClassLoader.defineClass1(Native Method)  
  15.     at java.lang.ClassLoader.defineClass(ClassLoader.java:620)  
  16.     at java.lang.ClassLoader.defineClass(ClassLoader.java:400)  
  17.     at WrongClassLoader.findClass(WrongClassLoader.java:43)  
  18.     at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
  19.     at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)  
  這說明,連要載入的型別的超型別java.lang.Object都載入不到了。這裡列舉的由於覆寫loadClass()引起的邏輯錯誤明顯是比較簡單的,實際引起的邏輯錯誤可能複雜的多。
[java] view plain copy
  1. //問題5測試二  
  2. //使用者自定義類載入器WrongClassLoader.Java(不覆寫loadClass邏輯)  
  3. public class WrongClassLoader extends ClassLoader {  
  4.   
  5.     protected Class<?> findClass(String name) throws ClassNotFoundException {  
  6.         //假設此處只是到工程以外的特定目錄D:\library下去載入類  
  7.         //具體實現程式碼省略  
  8.     }  
  9. }  
  將自定義類載入器程式碼WrongClassLoader.Java做以上修改後,再執行測試程式碼,輸出結果如下:
[plain] view plain copy
  1. beans.Account  
  2. WrongClassLoader@1c78e57  
  2、正確設定父類載入器
  通過上面問題4和問題5的分析我們應該已經理解,個人覺得這是自定義使用者類載入器時最重要的一點,但常常被忽略或者輕易帶過。有了前面JDK程式碼的分析作為基礎,我想現在大家都可以隨便舉出例子了。
  3、保證findClass(String name)方法的邏輯正確性
  事先儘量準確理解待定義的類載入器要完成的載入任務,確保最大程度上能夠獲取到對應的位元組碼內容。


4.6 如何在執行時判斷系統類載入器能載入哪些路徑下的類?


  一是可以直接呼叫ClassLoader.getSystemClassLoader()或者其他方式獲取到系統類載入器(系統類載入器和擴充套件類載入器本身都派生自URLClassLoader),呼叫URLClassLoader中的getURLs()方法可以獲取到。
  二是可以直接通過獲取系統屬性java.class.path來檢視當前類路徑上的條目資訊 :System.getProperty("java.class.path")。


4.7 如何在執行時判斷標準擴充套件類載入器能載入哪些路徑下的類?


  方法之一:

[java] view plain copy
  1. import java.net.URL;  
  2. import java.net.URLClassLoader;  
  3.   
  4. public class ClassLoaderTest {  
  5.   
  6.     /** 
  7.      * @param args the command line arguments 
  8.      */  
  9.     public static void main(String[] args) {  
  10.         try {  
  11.             URL[] extURLs = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();  
  12.             for (int i = 0; i < extURLs.length; i++) {  
  13.                 System.out.println(extURLs[i]);  
  14.             }  
  15.         } catch (Exception e) {  
  16.             //…  
  17.         }  
  18.     }  
  19. }  
  本機對應輸出如下:
[plain] view plain copy
  1. file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/access-bridge-64.jar  
  2. file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/cldrdata.jar  
  3. file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/dnsns.jar  
  4. file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/jaccess.jar  
  5. file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/jfxrt.jar  
  6. file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/localedata.jar  
  7. file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/nashorn.jar  
  8. file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunec.jar  
  9. file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunjce_provider.jar  
  10. file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunmscapi.jar  
  11. file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunpkcs11.jar  
  12. file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/zipfs.jar  


5 開發自己的類載入器


  在前面介紹類載入器的代理委派模式的時候,提到過類載入器會首先代理給其它類載入器來嘗試載入某個類。這就意味著真正完成類的載入工作的類載入器和啟動這個載入過程的類載入器,有可能不是同一個。真正完成類的載入工作是通過呼叫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虛擬機器中執行的類來。下面將通過兩個具體的例項來說明類載入器的開發。


5.1 檔案系統類載入器


  第一個類載入器用來載入儲存在檔案系統上的Java位元組程式碼。完整的實現如下所示。

[java] view plain copy
  1. package classloader;  
  2.   
  3. import java.io.ByteArrayOutputStream;  
  4. import java.io.File;  
  5. import java.io.FileInputStream;  
  6. import java.io.IOException;  
  7. import java.io.InputStream;  
  8.   
  9. // 檔案系統類載入器  
  10. public class FileSystemClassLoader extends ClassLoader {  
  11.   
  12.     private String rootDir;  
  13.   
  14.     public FileSystemClassLoader(String rootDir) {  
  15.         this.rootDir = rootDir;  
  16.     }  
  17.   
  18.     // 獲取類的位元組碼  
  19.     @Override  
  20.     protected Class<?> findClass(String name) throws ClassNotFoundException {  
  21.         byte[] classData = getClassData(name);  // 獲取類的位元組陣列  
  22.         if (classData == null) {  
  23.             throw new ClassNotFoundException();  
  24.         } else {  
  25.             return defineClass(name, classData, 0, classData.length);  
  26.         }  
  27.     }  
  28.   
  29.     private byte[] getClassData(String className) {  
  30.         // 讀取類檔案的位元組  
  31.         String path = classNameToPath(className);  
  32.         try {  
  33.             InputStream ins = new FileInputStream(path);  
  34.             ByteArrayOutputStream baos = new ByteArrayOutputStream();  
  35.             int bufferSize = 4096;  
  36.             byte[] buffer = new byte[bufferSize];  
  37.             int bytesNumRead = 0;  
  38.             // 讀取類檔案的位元組碼  
  39.             while ((bytesNumRead = ins.read(buffer)) != -1) {  
  40.                 baos.write(buffer, 0, bytesNumRead);  
  41.             }  
  42.             return baos.toByteArray();  
  43.         } catch (IOException e) {  
  44.             e.printStackTrace();  
  45.         }  
  46.         return null;  
  47.     }  
  48.   
  49.     private String classNameToPath(String className) {  
  50.         // 得到類檔案的完全路徑  
  51.         return rootDir + File.separatorChar  
  52.                 + className.replace('.', File.separatorChar) + ".class";  
  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類的例項。

  載入本地檔案系統上的類,示例如下:

[java] view plain copy
  1. package com.example;  
  2.   
  3. public class Sample {  
  4.   
  5.     private Sample instance;  
  6.   
  7.     public void setSample(Object instance) {  
  8.         System.out.println(instance.toString());  
  9.         this.instance = (Sample) instance;  
  10.     }  
  11. }  
[java] view plain copy
  1. package classloader;  
  2.   
  3. import java.lang.reflect.Method;  
  4.   
  5. public class ClassIdentity {  
  6.   
  7.     public static void main(String[] args) {  
  8.         new ClassIdentity().testClassIdentity();  
  9.     }  
  10.   
  11.     public void testClassIdentity() {  
  12.         String classDataRootPath = "C:\\Users\\JackZhou\\Documents\\NetBeansProjects\\classloader\\build\\classes";  
  13.         FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);  
  14.         FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);  
  15.         String className = "com.example.Sample";  
  16.         try {  
  17.             Class<?> class1 = fscl1.loadClass(className);  // 載入Sample類  
  18.             Object obj1 = class1.newInstance();  // 建立物件  
  19.             Class<?> class2 = fscl2.loadClass(className);  
  20.             Object obj2 = class2.newInstance();  
  21.             Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);  
  22.             setSampleMethod.invoke(obj1, obj2);  
  23.         } catch (Exception e) {  
  24.             e.printStackTrace();  
  25.         }  
  26.     }  
  27. }  
  執行輸出:com.example.Sample@7852e922


5.2 網路類載入器


  下面將通過一個網路類載入器來說明如何通過類載入器來實現元件的動態更新。即基本的場景是:Java 位元組程式碼(.class)檔案存放在伺服器上,客戶端通過網路的方式獲取位元組程式碼並執行。當有版本更新的時候,只需要替換掉伺服器上儲存的檔案即可。通過類載入器可以比較簡單的實現這種需求。
  類 NetworkClassLoader負責通過網路下載Java類位元組程式碼並定義出Java類。它的實現與FileSystemClassLoader類似。

[java] view plain copy
  1. package classloader;  
  2.   
  3. import java.io.ByteArrayOutputStream;  
  4. import java.io.InputStream;  
  5. import java.net.URL;  
  6.   
  7. public class NetworkClassLoader extends ClassLoader {  
  8.   
  9.     private String rootUrl;  
  10.   
  11.     public NetworkClassLoader(String rootUrl) {  
  12.         // 指定URL  
  13.         this.rootUrl = rootUrl;  
  14.     }  
  15.   
  16.     // 獲取類的位元組碼  
  17.     @Override  
  18.     protected Class<?> findClass(String name) throws ClassNotFoundException {  
  19.         byte[] classData = getClassData(name);  
  20.         if (classData == null) {  
  21.             throw new ClassNotFoundException();  
  22.         } else {  
  23.             return defineClass(name, classData, 0, classData.length);  
  24.         }  
  25.     }  
  26.   
  27.     private byte[] getClassData(String className) {  
  28.         // 從網路上讀取的類的位元組  
  29.         String path = classNameToPath(className);  
  30.         try {  
  31.             URL url = new URL(path);  
  32.             InputStream ins = url.openStream();  
  33.             ByteArrayOutputStream baos = new ByteArrayOutputStream();  
  34.             int bufferSize = 4096;  
  35.             byte[] buffer = new byte[bufferSize];  
  36.             int bytesNumRead = 0;  
  37.             // 讀取類檔案的位元組  
  38.             while ((bytesNumRead = ins.read(buffer)) != -1) {  
  39.                 baos.write(buffer, 0, bytesNumRead);  
  40.             }  
  41.             return baos.toByteArray();  
  42.         } catch (Exception e) {  
  43.             e.printStackTrace();  
  44.         }  
  45.         return null;  
  46.     }  
  47.   
  48.     private String classNameToPath(String className) {  
  49.         // 得到類檔案的URL  
  50.         return rootUrl + "/"  
  51.                 + className.replace('.''/') + ".class";  
  52.     }  
  53. }  
  在通過NetworkClassLoader載入了某個版本的類之後,一般有兩種做法來使用它。第一種做法是使用Java反射API。另外一種做法是使用介面。需要注意的是,並不能直接在客戶端程式碼中引用從伺服器上下載的類,因為客戶端程式碼的類載入器找不到這些類。使用Java反射API可以直接呼叫Java類的方法。而使用介面的做法則是把介面的類放在客戶端中,從伺服器上載入實現此介面的不同版本的類。在客戶端通過相同的介面來使用這些實現類。我們使用介面的方式。示例如下:

  客戶端介面:

[java] view plain copy
  1. package classloader;  
  2.   
  3. public interface Versioned {  
  4.   
  5.     String getVersion();  
  6. }  

[java] view plain copy
  1. package classloader;  
  2.   
  3. public interface ICalculator extends Versioned {  
  4.   
  5.     String calculate(String expression);  
  6. }  
  網路上的不同版本的類:
[java] view plain copy
  1. package com.example;  
  2.   
  3. import classloader.ICalculator;  
  4.   
  5. public class CalculatorBasic implements ICalculator {  
  6.   
  7.     @Override  
  8.     public String calculate(String expression) {  
  9.         return expression;  
  10.     }  
  11.   
  12.     @Override  
  13.     public String getVersion() {  
  14.         return "1.0";  
  15.     }  
  16.   
  17. }  
[java] view plain copy
  1. package com.example;  
  2.   
  3. import classloader.ICalculator;  
  4.   
  5. public class CalculatorAdvanced implements ICalculator {  
  6.   
  7.     @Override  
  8.     public String calculate(String expression) {  
  9.         return "Result is " + expression;  
  10.     }  
  11.   
  12.     @Override  
  13.     public String getVersion() {  
  14.         return "2.0";  
  15.     }  
  16.   
  17. }  
  在客戶端載入網路上的類的過程:
[java] view plain copy
  1. package classloader;  
  2.   
  3. public class CalculatorTest {  
  4.   
  5.     public static void main(String[] args) {  
  6.         String url = "http://localhost:8080/ClassloaderTest/classes";  
  7.         NetworkClassLoader ncl = new NetworkClassLoader(url);  
  8.         String basicClassName = "com.example.CalculatorBasic";  
  9.         String advancedClassName = "com.example.CalculatorAdvanced";  
  10.         try {  
  11.             Class<?> clazz = ncl.loadClass(basicClassName);  // 載入一個版本的類  
  12.             ICalculator calculator = (ICalculator) clazz.newInstance();  // 建立物件  
  13.             System.out.println(calculator.getVersion());  
  14.             clazz = ncl.loadClass(advancedClassName);  // 載入另一個版本的類  
  15.             calculator = (ICalculator) clazz.newInstance();  
  16.             System.out.println(calculator.getVersion());  
  17.         } catch (Exception e) {  
  18.             e.printStackTrace();  
  19.         }  
  20.     }  
  21.   
  22. }  

相關文章