每一個開發人員對Java.lang.ClassNotFoundExcetpion這個異常確定都不陌生,這背後就涉及到了java技術體系中的類加載。Java的類加載機制是技術體系中比較核心的部分,雖然和大部分開發人員直接打交道很少,可是對其背後的機理有必定理解有助於排查程序中出現的類加載失敗等技術問題,對理解java虛擬機的鏈接模型和java語言的動態性都有很大幫助。java
咱們首先看一下JVM預約義的三種類型類加載器,當一個 JVM啓動的時候,Java缺省開始使用以下三種類型類裝入器:數據庫
啓動(Bootstrap)類加載器:引導類裝入器是用本地代碼實現的類裝入器,它負責將 <Java_Runtime_Home>/lib下面的核心類庫或-Xbootclasspath選項指定的jar包加載到內存中。因爲引導類加載器涉及到虛擬機本地實現細節,開發者沒法直接獲取到啓動類加載器的引用,因此不容許直接經過引用進行操做。apache
擴展(Extension)類加載器:擴展類加載器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將< Java_Runtime_Home >/lib/ext或者由系統變量-Djava.ext.dir指定位置中的類庫加載到內存中。開發者能夠直接使用標準擴展類加載器。api
系統(System)類加載器:系統類加載器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。它負責將系統類路徑java -classpath或-Djava.class.path變量所指的目錄下的類庫加載到內存中。開發者能夠直接使用系統類加載器。數組
除了以上列舉的三種類加載器,還有一種比較特殊的類型就是線程上下文類加載器,這個將在後面單獨介紹。緩存
在這裏,須要着重說明的是,JVM在加載類時默認採用的是雙親委派機制。通俗的講,就是某個特定的類加載器在接到加載類的請求時,首先將加載任務委託給父類加載器,依次遞歸,若是父類加載器能夠完成類加載任務,就成功返回;只有父類加載器沒法完成此加載任務時,才本身去加載。關於虛擬機默認的雙親委派機制,咱們能夠從系統類加載器和擴展類加載器爲例做簡單分析。安全
圖一 標準擴展類加載器繼承層次圖網絡
圖二系統類加載器繼承層次圖數據結構
經過圖一和圖二咱們能夠看出,類加載器均是繼承自java.lang.ClassLoader抽象類。咱們下面咱們就看簡要介紹一下java.lang.ClassLoader中幾個最重要的方法:ide
//加載指定名稱(包括包名)的二進制類型,供用戶調用的接口 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方法中讀取到對應字節碼後調用,能夠看出不可繼承 //(說明:JVM已經實現了對應的具體功能,解析對應的字節碼,產生對應的內部數據結構放置到方法區,因此無需覆寫,直接調用就能夠了) protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ … }
經過進一步分析標準擴展類加載器(sun.misc.Launcher$ExtClassLoader)和系統類加載器(sun.misc.Launcher$AppClassLoader)的代碼以及其公共父類(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 { //若是不存在父類加載器,就檢查是不是由啓動類加載器加載的類, //經過調用本地方法native findBootstrapClass0(String name) c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { // 若是父類加載器和啓動類加載器都不能完成加載任務,才調用自身的加載功能 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; }
經過上面的代碼分析,咱們能夠對JVM採用的雙親委派類加載機制有了更感性的認識,下面咱們就接着分析一下啓動類加載器、標準擴展類加載器和系統類加載器三者之間的關係。可能你們已經從各類資料上面看到了以下相似的一幅圖片:
上面圖片給人的直觀印象是系統類加載器的父類加載器是標準擴展類加載器,標準擴展類加載器的父類加載器是啓動類加載器,下面咱們就用代碼具體測試一下:
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()能夠直接獲取到系統類加載器。
代碼輸出以下:
sun.misc.Launcher$AppClassLoader@6d06d69c sun.misc.Launcher$ExtClassLoader@70dea4e null
經過以上的代碼輸出,咱們能夠斷定系統類加載器的父加載器是標準擴展類加載器,可是咱們試圖獲取標準擴展類加載器的父類加載器時確獲得了null,就是說標準擴展類加載器自己強制設定父類加載器爲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; }
咱們再看一下ClassLoader抽象類中parent成員的聲明:
// The parent class loader for delegation 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,則會調用本地方法進行啓動類加載嘗試。因此,圖三中,啓動類加載器、標準擴展類加載器和系統類加載器之間的委派關係事實上是仍就成立的。(在後面的用戶自定義類加載器部分,還會作更深刻的分析)。
以上已經簡要介紹了虛擬機默認使用的啓動類加載器、標準擴展類加載器和系統類加載器,並以三者爲例結合JDK代碼對JVM默認使用的雙親委派類加載機制作了分析。下面咱們就來看一個綜合的例子。首先在IDE中創建一個簡單的java應用工程,而後寫一個簡單的JavaBean以下:
package classloader.test.bean; public class TestBean { public TestBean() { } }
在現有當前工程中另外創建一測試類(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(); } } }
對應的輸出以下:
C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes
sun.misc.Launcher$AppClassLoader@73d16e93
說明:當前類路徑默認的含有的一個條目就是工程的輸出目錄。
測試二:
C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes
sun.misc.Launcher$ExtClassLoader@15db9742
對比測試一和測試二,咱們明顯能夠驗證前面說的雙親委派機制,系統類加載器在接到加載classloader.test.bean.TestBean類型的請求時,首先將請求委派給父類加載器(標準擴展類加載器),標準擴展類加載器搶先完成了加載請求。
測試三:
將test.jar拷貝一份到<Java_Runtime_Home>/lib下,運行測試代碼,輸出以下:
C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes
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中變量視圖查看驗證。
Java的鏈接模型容許用戶運行時擴展引用程序,既能夠經過當前虛擬機中預約義的加載器加載編譯時已知的類或者接口,又容許用戶自行定義類裝載器,在運行時動態擴展用戶的程序。經過用戶自定義的類裝載器,你的程序能夠裝載在編譯時並不知道或者還沒有存在的類或者接口,並動態鏈接它們並進行有選擇的解析。
運行時動態擴展java應用程序有以下兩個途徑:
這個方法其實在前面已經討論過,在後面的問題2解答中說明了該方法調用會觸發哪一個類加載器開始加載任務。這裏須要說明的是多參數版本的forName(…)方法:
將當前工程輸出目錄下的TestBean.class打包進test.jar剪貼到<Java_Runtime_Home>/lib/ext目錄下(如今工程輸出目錄下和JRE擴展目錄下都有待加載類型的class文件)。再運行測試一測試代碼,結果以下:
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 數據庫的驅動。
經過前面的分析,咱們能夠看出,除了和本地實現密切相關的啓動類加載器以外,包括標準擴展類加載器和系統類加載器在內的全部其餘類加載器咱們均可以當作自定義類加載器來對待,惟一區別是是否被虛擬機默認使用。前面的內容中已經對java.lang.ClassLoader抽象類中的幾個重要的方法作了介紹,這裏就簡要敘述一下通常用戶自定義類加載器的工做流程吧(能夠結合後面問題解答一塊兒看):
一、首先檢查請求的類型是否已經被這個類裝載器裝載到命名空間中了,若是已經裝載,直接返回;不然轉入步驟2;
二、委派類加載請求給父類加載器(更準確的說應該是雙親類加載器,真實虛擬機中各類類加載器最終會呈現樹狀結構),若是父類加載器可以完成,則返回父類加載器加載的Class實例;不然轉入步驟3;
三、調用本類加載器的findClass(…)方法,試圖獲取對應的字節碼,若是獲取的到,則調用defineClass(…)導入類型到方法區;若是獲取不到對應的字節碼或者其餘緣由失敗,返回異常給loadClass(…), loadClass(…)轉而拋異常,終止加載過程(注意:這裏的異常種類不止一種)。
說明:這裏說的自定義類加載器是指JDK 1.2之後版本的寫法,即不覆寫改變java.lang.loadClass(…)已有委派邏輯狀況下。
整個加載類的過程以下圖:
圖六 自定義類加載器加載類的過程
在Java中,一個類用其徹底匹配類名(fully qualified class name)做爲標識,這裏指的徹底匹配類名包括包名和類名。但在JVM中一個類用其全名和一個加載類ClassLoader的實例做爲惟一標識,不一樣類加載器加載的類將被置於不一樣的命名空間。咱們能夠用兩個自定義類加載器去加載某自定義類型(注意不要將自定義類型的字節碼放置到系統路徑或者擴展路徑中,不然會被系統類加載器或擴展類加載器搶先加載),而後用獲取到的兩個Class實例進行java.lang.Object.equals(…)判斷,將會獲得不相等的結果。這個你們能夠寫兩個自定義的類加載器去加載相同的自定義類型,而後作個判斷;同時,能夠測試加載java.*類型,而後再對比測試一下測試結果。
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();
前面講過,在不指定父類加載器的狀況下,默認採用系統類加載器。可能有人以爲不明白,如今咱們來看一下JDK對應的代碼實現。衆所周知,咱們編寫自定義的類加載器直接或者間接繼承自java.lang.ClassLoader抽象類,對應的無參默認構造函數實現以下:
//摘自java.lang.ClassLoader.java protected ClassLoader() { SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkCreateClassLoader(); } this.parent = getSystemClassLoader(); initialized = true; }
咱們再來看一下對應的getSystemClassLoader()方法的實現:
private static synchronized void initSystemClassLoader() { //... sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); scl = l.getClassLoader(); //... }
咱們能夠寫簡單的測試代碼來測試一下:
System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());
本機對應輸出以下:
sun.misc.Launcher$AppClassLoader@73d16e93
因此,咱們如今能夠相信當自定義類加載器沒有指定父類加載器的狀況下,默認的父類加載器即爲系統類加載器。同時,咱們能夠得出以下結論:即便用戶自定義類加載器不指定父類加載器,那麼,一樣能夠加載以下三個地方的類:
1. <Java_Runtime_Home>/lib下的類;
2. < Java_Runtime_Home >/lib/ext下或者由系統變量java.ext.dir指定位置中的類;
3. 當前工程類路徑下或者由系統變量java.class.path指定位置中的類。
JVM規範中規定若是用戶自定義的類加載器將父類加載器強制設置爲null,那麼會自動將啓動類加載器設置爲當前用戶自定義類加載器的父類加載器(這個問題前面已經分析過了)。同時,咱們能夠得出以下結論:
即便用戶自定義類加載器不指定父類加載器,那麼,一樣能夠加載到<Java_Runtime_Home>/lib下的類,但此時就不可以加載<Java_Runtime_Home>/lib/ext目錄下的類了。
說明:問題3和問題4的推斷結論是基於用戶自定義的類加載器自己延續了java.lang.ClassLoader.loadClass(…)默認委派邏輯,若是用戶對這一默認委派邏輯進行了改變,以上推斷結論就不必定成立了,詳見問題5。
一、通常儘可能不要覆寫已有的loadClass(...)方法中的委派邏輯
通常在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下去加載類 // 具體實現代碼省略 } }
經過前面的分析咱們已經知道,這個自定義類加載器WrongClassLoader的默認類加載器是系統類加載器,可是如今問題4種的結論就不成立了。你們能夠簡單測試一下,如今<Java_Runtime_Home>/lib、< Java_Runtime_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(); } } }
這裏D:"classes"beans"Account.class是物理存在的。輸出結果:
java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系統找不到指定的路徑。) 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()引發的邏輯錯誤明顯是比較簡單的,實際引發的邏輯錯誤可能複雜的多。
//問題5測試二 //用戶自定義類加載器WrongClassLoader.Java(不覆寫loadClass邏輯) public class WrongClassLoader extends ClassLoader { protected Class<?> findClass(String name) throws ClassNotFoundException { //假設此處只是到工程之外的特定目錄D:\library下去加載類 //具體實現代碼省略 } }
將自定義類加載器代碼WrongClassLoader.Java作以上修改後,再運行測試代碼,輸出結果以下:
beans.Account
WrongClassLoader@1c78e57
二、正確設置父類加載器
經過上面問題4和問題5的分析咱們應該已經理解,我的以爲這是自定義用戶類加載器時最重要的一點,但經常被忽略或者輕易帶過。有了前面JDK代碼的分析做爲基礎,我想如今你們均可以隨便舉出例子了。
三、保證findClass(String name)方法的邏輯正確性
事先儘可能準確理解待定義的類加載器要完成的加載任務,確保最大程度上可以獲取到對應的字節碼內容。
一是能夠直接調用ClassLoader.getSystemClassLoader()或者其餘方式獲取到系統類加載器(系統類加載器和擴展類加載器自己都派生自URLClassLoader),調用URLClassLoader中的getURLs()方法能夠獲取到。
二是能夠直接經過獲取系統屬性java.class.path來查看當前類路徑上的條目信息 :System.getProperty("java.class.path")。
方法之一:
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) { //… } } }
本機對應輸出以下:
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
在前面介紹類加載器的代理委派模式的時候,提到過類加載器會首先代理給其它類加載器來嘗試加載某個類。這就意味着真正完成類的加載工做的類加載器和啓動這個加載過程的類加載器,有可能不是同一個。真正完成類的加載工做是經過調用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虛擬機中運行的類來。下面將經過兩個具體的實例來講明類加載器的開發。
第一個類加載器用來加載存儲在文件系統上的Java字節代碼。完整的實現以下所示。
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"; } }
如上所示,類 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; } }
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(); } } }
運行輸出:com.example.Sample@7852e922