Spring原始碼學習之:ClassLoader學習(3)

Love Lenka發表於2016-11-07

ClassLoader主要對類的請求提供服務,當JVM需要某類時,它根據名稱向ClassLoader要求這個類,然後由ClassLoader返回 這個類的class物件。 1.1 幾個相關概念ClassLoader負責載入系統的所有Resources(Class,檔案,來自網路的位元組流 等),透過ClassLoader從而將資源載入JVM 
每個class都有一個reference,指向自己的ClassLoader。Class.getClassLoader() 
array的ClassLoader就是其元素的ClassLoader,若是基本資料型別,則這個array沒有ClassLoader 
1.2 主要方法和工作過程Java1.1及從前版本中,ClassLoader主要方法: 
Class loadClass( String name, boolean resolve ); ClassLoader.loadClass() 是 ClassLoader 的入口點 
defineClass 方法是 ClassLoader 的主要訣竅。該方法接受由原始位元組組成的陣列並把它轉換成 Class 物件。原始陣列包含如從檔案系統或網路裝入的資料。 
findSystemClass 方 法從本地檔案系統裝入檔案。它在本地檔案系統中尋找類檔案,如果存在,就使用 defineClass 將原始位元組轉換成 Class 物件,以將該檔案 轉換成類。當執行 Java 應用程式時,這是 JVM 正常裝入類的預設機制。 
resolveClass可以不完全地(不帶解析)裝入類,也可以完全地(帶解析)裝入類。當編寫我們自己的 loadClass 時,可以呼叫 resolveClass,這取決於 loadClass 的 resolve 引數的值 
findLoadedClass 充當一個快取:當請求 loadClass 裝入類時,它呼叫該方法來檢視 ClassLoader 是否已裝入這個類,這樣可以避免重新裝入已存在類所造成的麻煩。應首先呼叫該方法 
一般load方法過程如下: 

呼叫 findLoadedClass 來檢視是否存在已裝入的類。 
如果沒有,那麼採用某種特殊的神奇方式來獲取原始位元組。(透過IO從檔案系統,來自網路的位元組流等) 
如果已有原始位元組,呼叫 defineClass 將它們轉換成 Class 物件。 
如果沒有原始位元組,然後呼叫 findSystemClass 檢視是否從本地檔案系統獲取類。 
如果 resolve 引數是 true,那麼呼叫 resolveClass 解析 Class 物件。 
如果還沒有類,返回 ClassNotFoundException。 
否則,將類返回給呼叫程式。 
1.3 委託模型自從JDK1.2以後,ClassLoader做了改進,使用了委託模型,所有系統中的ClassLoader組成一棵樹,ClassLoader在載入類庫時先讓Parent尋找,Parent找不到才自己找。 
JVM 在執行時會產生三個ClassLoader,Bootstrap ClassLoader、Extension ClassLoader和 App ClassLoader。其中,Bootstrap ClassLoader是用C++編寫的,在Java中看不到它,是null。它用來載入核 心類庫,就是在lib下的類庫,Extension ClassLoader載入lib/ext下的類庫,App ClassLoader載入 Classpath裡的類庫,三者的關係為:App ClassLoader的Parent是Extension ClassLoader,而 Extension ClassLoader的Parent為Bootstrap ClassLoader。載入一個類時,首先BootStrap進行尋 找,找不到再由Extension ClassLoader尋找,最後才是App ClassLoader。 

將 ClassLoader設計成委託模型的一個重要原因是出於安全考慮,比如在Applet中,如果編寫了一個java.lang.String類並具有破 壞性。假如不採用這種委託機制,就會將這個具有破壞性的String載入到了使用者機器上,導致破壞使用者安全。但採用這種委託機制則不會出現這種情況。因為 要載入java.lang.String類時,系統最終會由Bootstrap進行載入,這個具有破壞性的String永遠沒有機會載入。 

委託模型還帶來了一些問題,在某些情況下會產生混淆,如下是Tomcat的ClassLoader結構圖: 

                Bootstrap
                  |
                System
                  |
                Common
                /    
            Catalina  Shared
                      /    
                   Webapp1  Webapp2 ...

由 Common 類 裝入器裝入的類決不能(根據名稱)直接訪問由 Web 應用程式裝入的類。使這些類聯絡在一起的唯一方法是透過使用這兩個類集都可見的介面。在這個例子 中,就是包含由 Java servlet 實現的 javax.servlet.Servlet。 
如果在lib或者lib/ext等類庫有 與應用中同樣的類,那麼應用中的類將無法被載入。通常在jdk新版本出現有類庫移動時會出現問題,例如最初我們使用自己的xml解析器,而在jdk1.4 中xml解析器變成標準類庫,load的優先順序也高於我們自己的xml解析器,我們自己的xml解析器永遠無法找到,將可能導致我們的應用無法執行。 

相同的類,不同的ClassLoader,將導致ClassCastException異常 

1.4 線 程中的ClassLoader每個執行中的執行緒都有一個成員contextClassLoader,用來在執行時動態地載入其它類,可以使用方法 Thread.currentThread().setContextClassLoader(...);更改當前執行緒的 contextClassLoader,來改變其載入類的行為;也可以透過方法 Thread.currentThread().getContextClassLoader()來獲得當前執行緒的ClassLoader。 
實際上,在Java應用中所有程式都執行線上程裡,如果在程式中沒有手工設定過ClassLoader,對於一般的java類如下兩種方法獲得的ClassLoader通常都是同一個 

this.getClass.getClassLoader(); 
Thread.currentThread().getContextClassLoader(); 
方 法一得到的Classloader是靜態的,表明類的載入者是誰;方法二得到的Classloader是動態的,誰執行(某個執行緒),就是那個執行者的 Classloader。對於單例模式的類,靜態類等,載入一次後,這個例項會被很多程式(執行緒)呼叫,對於這些類,載入的Classloader和執行 執行緒的Classloader通常都不同。 

1.5 Web應用中的ClassLoader回到上面的例子,在Tomcat 裡,WebApp的ClassLoader的工作原理有點不同,它先試圖自己載入類(在ContextPath/WEB-INF/...中載入類),如果 無法載入,再請求父ClassLoader完成。 
由此可得: 

對於WEB APP執行緒,它的contextClassLoader是WebAppClassLoader 
對於Tomcat Server執行緒,它的contextClassLoader是CatalinaClassLoader 
1.6 獲得ClassLoader的幾種方法可以透過如下3種方法得到ClassLoader 
this.getClass.getClassLoader(); // 使用當前類的ClassLoader 
Thread.currentThread().getContextClassLoader(); // 使用當前執行緒的ClassLoader 
ClassLoader.getSystemClassLoader(); // 使 用系統ClassLoader,即系統的入口點所使用的ClassLoader。(注意,system ClassLoader與根 ClassLoader並不一樣。JVM下system ClassLoader通常為App ClassLoader) 
1.7 幾種擴充套件應用使用者定製自己的ClassLoader可以實現以下的一些應用 
安全性。類進入JVM之前先經過ClassLoader,所以可以在這邊檢查是否有正確的數字簽名等 
加密。java位元組碼很容易被反編譯,透過定製ClassLoader使得位元組碼先加密防止別人下載後反編譯,這裡的ClassLoader相當於一個動態的解碼器 
歸檔。可能為了節省網路資源,對自己的程式碼做一些特殊的歸檔,然後用定製的ClassLoader來解檔 
自展開程式。把java應用程式編譯成單個可執行類檔案,這個檔案包含壓縮的和加密的類檔案資料,同時有一個固定的ClassLoader,當程式執行時它在記憶體中完全自行解開,無需先安裝 
動態生成。可以生成應用其他還未生成類的類,實時建立整個類並可在任何時刻引入JVM 
2.0 資源載入
所 有資源都透過ClassLoader載入到JVM裡,那麼在載入資源時當然可以使用ClassLoader,只是對於不同的資源還可以使用一些別的方式載 入,例如對於類可以直接new,對於檔案可以直接做IO等。 2.1 載入類的幾種方法假設有類A和類B,A在方法amethod裡需要例項化B,可能的 方法有3種。對於載入類的情況,使用者需要知道B類的完整名字(包括包名,例如"com.rain.B") 
1. 使用Class靜態方法 Class.forName 

    Class cls = Class.forName("com.rain.B");
    B b = (B)cls.newInstance();

2. 使用ClassLoader 
    /* Step 1. Get ClassLoader */
    ClassLoader cl; // 如何獲得ClassLoader參考1.6

    /* Step 2. Load the class */
    Class cls = cl.loadClass("com.rain.B"); // 使用第一步得到的ClassLoader來載入B
    
    /* Step 3. new instance */
    B b = (B)cls.newInstance(); // 有B的類得到一個B的例項

3. 直接new 
    B b = new B();

2.2 文 件載入(例如配置檔案等)假設在com.rain.A類裡想讀取資料夾 /com/rain/config 裡的檔案sys.properties,讀取 檔案可以透過絕對路徑或相對路徑,絕對路徑很簡單,在Windows下以盤號開始,在Unix下以"/"開始 
對於相對路徑,其相對值是相對於ClassLoader的,因為ClassLoader是一棵樹,所以這個相對路徑和ClassLoader樹上的任何一個ClassLoader相對比較後可以找到檔案,那麼檔案就可以找到,當然,讀取檔案也使用委託模型 

1. 直接IO 

/**
 * 假設當前位置是 "C:/test",透過執行如下命令來執行A "java com.rain.A"
 * 1. 在程式裡可以使用絕對路徑,Windows下的絕對路徑以盤號開始,Unix下以"/"開始
 * 2. 也可以使用相對路徑,相對路徑前面沒有"/"
 * 因為我們在 "C:/test" 目錄下執行程式,程式入口點是"C:/test",相對路徑就
 * 是 "com/rain/config/sys.properties"
 * (例子中,當前程式的ClassLoader是App ClassLoader,system ClassLoader = 當前的
 * 程式的ClassLoader,入口點是"C:/test")
 * 對於ClassLoader樹,如果檔案在jdk lib下,如果檔案在jdk lib/ext下,如果檔案在環境變數裡,
 * 都可以透過相對路徑"sys.properties"找到,lib下的檔案最先被找到
 */
File f = new File("C:/test/com/rain/config/sys.properties"); // 使用絕對路徑
//File f = new File("com/rain/config/sys.properties"); // 使用相對路徑
InputStream is = new FileInputStream(f);

如果是配置檔案,可以透過java.util.Properties.load(is)將內容讀到Properties裡,Properties預設認為is的編碼是ISO-8859-1,如果配置檔案是非英文的,可能出現亂碼問題。 
2. 使用ClassLoader 

/**
 * 因為有3種方法得到ClassLoader,對應有如下3種方法讀取檔案
 * 使用的路徑是相對於這個ClassLoader的那個點的相對路徑,此處只能使用相對路徑
 */
InputStream is = null;
is = this.getClass().getClassLoader().getResourceAsStream(
       "com/rain/config/sys.properties"); //方法1
//is = Thread.currentThread().getContextClassLoader().getResourceAsStream(
       "com/rain/config/sys.properties"); //方法2
//is = ClassLoader.getSystemResourceAsStream("com/rain/config/sys.properties"); //方法3

如果是配置檔案,可以透過java.util.Properties.load(is)將內容讀到Properties裡,這裡要注意編碼問題。 
3. 使用ResourceBundle 

    ResourceBundle bundle = ResourceBundle.getBoundle("com.rain.config.sys");

這種用法通常用來載入使用者的配置檔案,關於ResourceBunlde更詳細的用法請參考其他文件 
總結:有如下3種途徑來載入檔案 

    1. 絕對路徑 ---> IO
    2. 相對路徑 ---> IO
                ---> ClassLoader
    3. 資原始檔 ---> ResourceBundle

2.3 如何在web應用裡載入資源在web應用裡當然也可以使用ClassLoader來載入資源,但更常用的情況是使用ServletContext,如下是web目錄結構 
    ContextRoot
       |- JSP、HTML、Image等各種檔案
        |- [WEB-INF]
              |- web.xml
              |- [lib] Web用到的JAR檔案
                |- [classes] 類檔案

使用者程式通常在classes目錄下,如果想讀取classes目錄裡的檔案,可以使用ClassLoader,如果想讀取其他的檔案,一般使用ServletContext.getResource() 
如 果使用ServletContext.getResource(path)方法,路徑必須以"/"開始,路徑被解釋成相對於ContextRoot的路 徑,此處載入檔案的方法和ClassLoader不同,舉例"/WEB-INF/web.xml","/download /WebExAgent.rar"

相關文章