深入理解Java類加載器(一):Java類加載原理解析

原文鏈接:https://blog.csdn.net/justloveyou_/article/details/72217806

一、引子

每個開發人員對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{ … }  

通過進一步分析標準擴展類加載器和系統類加載器的代碼以及其公共父類(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;  
}  

通過上面的代碼分析,我們可以對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.Launcher$AppClassLoader@6d06d69c  
        sun.misc.Launcher$ExtClassLoader@70dea4e  
        null  
 *///:~

通過以上的代碼輸出,我們知道:通過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;  
}  

緊接着,我們再看一下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(null 本身就代表着引導類加載器)。(因爲如果不強制設置,默認會通過調用getSystemClassLoader()方法獲取並設置成系統類加載器,這顯然和測試輸出結果不符。)

事實上,這就是啓動類加載器、標準擴展類加載器和系統類加載器之間的委派關係。


3、類加載雙親委派示例

以上已經簡要介紹了虛擬機默認使用的啓動類加載器、標準擴展類加載器和系統類加載器,並以三者爲例結合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();  
        }  
    }  
}/* Output: 
        I:\AlgorithmPractice\TestClassLoader\bin
        sun.misc.Launcher$AppClassLoader@6150818a
 *///:~  

將當前工程輸出目錄下的TestBean.class打包進test.jar剪貼到<Java_Runtime_Home>/lib/ext目錄下(現在工程輸出目錄下和JRE擴展目錄下都有待加載類型的class文件)。再運行測試一測試代碼,結果如下:

    I:\AlgorithmPractice\TestClassLoader\bin
    sun.misc.Launcher$ExtClassLoader@15db9742

對比上面的兩個結果,我們明顯可以驗證前面說的雙親委派機制:系統類加載器在接到加載classloader.test.bean.TestBean類型的請求時,首先將請求委派給父類加載器(標準擴展類加載器),標準擴展類加載器搶先完成了加載請求。

最後,將test.jar拷貝一份到<Java_Runtime_Home>/lib下,運行測試代碼,輸出如下:

    I:\AlgorithmPractice\TestClassLoader\bin
    sun.misc.Launcher$ExtClassLoader@15db9742

可以看到,後兩次輸出結果一致。那就是說,放置到<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  

這裏的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!");
        }
    }
}
  •  

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  
 *///:~    

我們發現,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(); 

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;  
} 

我們再來看一下對應的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 
  •  

所以,我們現在可以相信當自定義類加載器沒有指定父類加載器的情況下,默認的父類加載器即爲系統類加載器。同時,我們可以得出如下結論:即使用戶自定義類加載器不指定父類加載器,那麼,同樣可以加載如下三個地方的類:

  • <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下去加載類  
        // 具體實現代碼省略  
    }  
}  

通過前面的分析我們已經知道,這個自定義類加載器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.<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)  
 *///:~    

注意,這裏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  
 *///:~  

將自定義類加載器代碼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
 *///:~ 

如上述程序所示,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
 *///:~ 
  •  

五. 開發自己的類加載器

在前面介紹類加載器的代理委派模型的時候,提到過類加載器會首先代理給其它類加載器來嘗試加載某個類,這就意味着真正完成類的加載工作的類加載器和啓動這個加載過程的類加載器,有可能不是同一個。真正完成類的加載工作是通過調用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";  
    }  
}  

如上所示,類 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();  
        }  
    }  
}/* Output: 
        com.example.Sample@7852e922
 *///:~   

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";  
    }  
}  

在通過NetworkClassLoader加載了某個版本的類之後,一般有兩種做法來使用它。第一種做法是使用Java反射API。另外一種做法是使用接口。需要注意的是,並不能直接在客戶端代碼中引用從服務器上下載的類,因爲客戶端代碼的類加載器找不到這些類。使用Java反射API可以直接調用Java類的方法。而使用接口的做法則是把接口的類放在客戶端中,從服務器上加載實現此接口的不同版本的類。在客戶端通過相同的接口來使用這些實現類。我們使用接口的方式。示例如下:


客戶端接口:

package classloader;  
  
public interface Versioned {  
  
    String getVersion();  
}   
package classloader;  
  
public interface ICalculator extends Versioned {  
  
    String calculate(String expression);  
}  

網絡上的不同版本的類:

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";  
    }  
}  

在客戶端加載網絡上的類的過程:

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();  
        }  
    }  
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章