1. 程式人生 > >ClassLoader,Thread.currentThread().setContextClassLoader,tomcat的ClassLoader

ClassLoader,Thread.currentThread().setContextClassLoader,tomcat的ClassLoader

實際上,在Java應用中所有程式都執行線上程裡,如果在程式中沒有手工設定過ClassLoader,對於一般的java類如下兩種方法獲得的ClassLoader通常都是同一個 

this.getClass.getClassLoader();  
Thread.currentThread().getContextClassLoader();  

方法一得到的Classloader是靜態的,表明類的載入者是誰;

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

一、執行緒上下文類載入器

  執行緒上下文類載入器(context class loader)是從 JDK 1.2 開始引入的。類 java.lang.Thread中的方法 getContextClassLoader()setContextClassLoader(ClassLoader cl)用來獲取和設定執行緒的上下文類載入器。如果沒有通過 setContextClassLoader(ClassLoader cl)方法進行設定的話,執行緒將繼承其父執行緒的上下文類載入器。Java 應用執行的初始執行緒的上下文類載入器是系統類載入器(appClassLoader)。線上程中執行的程式碼可以通過此類載入器來載入類和資源。

  前面提到的類載入器的代理模式並不能解決 Java 應用開發中會遇到的類載入器的全部問題。Java 提供了很多服務提供者介面(Service Provider Interface,SPI),允許第三方為這些介面提供實現。常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的介面由 Java 核心庫來提供,如 JAXP 的 SPI 介面定義包含在 javax.xml.parsers包中。這些 SPI 的實現程式碼很可能是作為 Java 應用所依賴的 jar 包被包含進來,可以通過類路徑(CLASSPATH)來找到,如實現了 JAXP SPI 的 Apache Xerces

所包含的 jar 包。SPI 介面中的程式碼經常需要載入具體的實現類。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory類中的 newInstance()方法用來生成一個新的DocumentBuilderFactory的例項。這裡的例項的真正的類是繼承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的實現所提供的。如在 Apache Xerces 中,實現的類是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl而問題在於,SPI 的介面是 Java 核心庫的一部分,是由引導類載入器來載入的;SPI 實現的 Java 類一般是由系統類載入器來載入的。引導類載入器是無法找到 SPI 的實現類的,因為它只加載 Java 的核心庫。它也不能代理給系統類載入器,因為它是系統類載入器的祖先類載入器。也就是說,類載入器的代理模式無法解決這個問題

  執行緒上下文類載入器正好解決了這個問題。如果不做任何的設定,Java 應用的執行緒的上下文類載入器預設就是系統上下文類載入器。在 SPI 介面的程式碼中使用執行緒上下文類載入器,就可以成功的載入到 SPI 實現的類。執行緒上下文類載入器在很多 SPI 的實現中都會用到。

JNDI,JDBC的訴求是:

為了能讓應用程式訪問到這些jar包中的實現類,即用appClassLoarder去載入這些實現類。可以用getContextClassLoader取得當前執行緒的ClassLoader(即appClassLoarder),然後去載入這些實現類,就能讓應用訪問到

tomcat的訴求:

稍微跟上面有些不同,容器不希望它下面的webapps之間能互相訪問到,所以不能用appClassLoarder去載入。所以tomcat新建一個sharedClassLoader(它的parent是commonClassLoader,commonClassLoader的parent是appClassLoarder,預設情況下,sharedClassLoader和commonClassLoader是同一個UrlClassLoader例項),這是catalina容器使用的ClassLoader。對於每個webapp,為其新建一個webappClassLoader,用於載入webapp下面的類,這樣webapp之間就不能相互訪問了。tomcat的ClassLoader不完全遵循雙親委派,首先用webappClassLoader去載入某個類,如果找不到,再交給parent。而對於java核心庫,不在tomcat的ClassLoader的載入範圍。

  看下tomcat的Bootstrap類的init方法:

複製程式碼
public void init() throws Exception {

        initClassLoaders();

        Thread.currentThread().setContextClassLoader(catalinaLoader);//不知道這行設定了之後,對後面有什麼用???

        SecurityClassLoad.securityClassLoad(catalinaLoader);

        // Load our startup class and call its process() method/*反射例項化Catalina類的例項*/
        Class<?> startupClass =
            catalinaLoader.loadClass
            ("org.apache.catalina.startup.Catalina");
        Object startupInstance = startupClass.newInstance();// Set the shared extensions class loader
        if (log.isDebugEnabled())
            log.debug("Setting startup class properties");
        String methodName = "setParentClassLoader";
        Class<?> paramTypes[] = new Class[1];
        paramTypes[0] = Class.forName("java.lang.ClassLoader");
        Object paramValues[] = new Object[1];
        paramValues[0] = sharedLoader;
        Method method =
            startupInstance.getClass().getMethod(methodName, paramTypes);
        method.invoke(startupInstance, paramValues);

        catalinaDaemon = startupInstance;

    }
複製程式碼

  由於Bootstrap類和catalina類被髮布在不同包裡面,Bootstrap對catalina例項的操作必須用反射完成。

  catalina類例項(即startupClass)由反射生成,它的ClassLoader是catalinaLoader。然後反射呼叫方法setParentClassLoader設定catalina類例項裡面的變數parentClassLoader為sharedClassLoader,意思是作為容器下webapp的webappClassLoader的parent,而不是設定catalina類的ClassLoader的parent是sharedClassLoader

  現在對tomcat的Bootstrap類的init方法裡面的Thread.currentThread().setContextClassLoader(catalinaLoader);這一行還是很疑惑。因為,在類catalina裡面,可以用getClass().getClassLoader()獲取catalinaClassLoader,不需要從Thread.currentThread().getContextClassLoader()方法獲得。難道是為了讓子執行緒的ClassLoader都是catalinaClassLoader,而不是appClassLoarder??

二、類載入器與 Web 容器

  對於執行在 Java EE™容器中的 Web 應用來說,類載入器的實現方式與一般的 Java 應用有所不同。不同的 Web 容器的實現方式也會有所不同。以 Apache Tomcat 來說,每個 Web 應用都有一個對應的類載入器例項。該類載入器也使用代理模式,所不同的是它是首先嚐試去載入某個類,如果找不到再代理給父類載入器。這與一般類載入器的順序是相反的。這是 Java Servlet 規範中的推薦做法,其目的是使得 Web 應用自己的類的優先順序高於 Web 容器提供的類。這種代理模式的一個例外是:Java 核心庫的類是不在查詢範圍之內的。這也是為了保證 Java 核心庫的型別安全

  絕大多數情況下,Web 應用的開發人員不需要考慮與類載入器相關的細節。下面給出幾條簡單的原則:

  • 每個 Web 應用自己的 Java 類檔案和使用的庫的 jar 包,分別放在 WEB-INF/classes和 WEB-INF/lib目錄下面。
  • 多個應用共享的 Java 類檔案和 jar 包,分別放在 Web 容器指定的由所有 Web 應用共享的目錄下面。
  • 當出現找不到類的錯誤時,檢查當前類的類載入器和當前執行緒的上下文類載入器是否正確。

三、ContextClassLoader和其他ClassLoader的關係 

  我們可以通過getContextClassLoader方法來獲得此context classloader,就可以用它來載入我們所需要的Class。預設的是system classloader。

  bootstrap classloader  -------  對應jvm中某c++寫的dll類
  Extenson ClassLoader ---------對應內部類ExtClassLoader
  System ClassLoader  ---------對應內部類AppClassLoader
  Custom ClassLoader  ----------對應任何URLClassLoader的子類(你也可以繼承SecureClassLoader或者更加nb一點 直接繼承ClassLoader,這樣的話你也是神一般的存在了 XD)

  以上四種classloder按照從上到下的順序,依次為下一個的parent

  這個第一概念

  第二個概念是幾個有關的classloader的類

抽象類 ClassLoader
                  |
            SecureClassLoader
                   |
            URLClassloader
             |           |                
 sun的ExtClassLoader   sun的AppClassLoader
  以上的類之間是繼承關係,與第一個概念說的parent是兩回事情,需要小心。

  第三個概念是Thread的ContextClassLoader
  其實從Context的名稱就可以看出來,這只是一個用以儲存任何classloader引用的臨時儲存空間,與classloader的層次沒有任何關係。

四、Context ClassLoader詳解

  通常情況下,類裝載器共有4種,即啟動類裝載器、EXT類裝載器、App類裝載器和自定義類裝載器。他們之間的階層情況如下圖左面所示,他們都有著不同的載入規則,並且通過向上代理的方式來進行。而本文所提到的Context Class Loader並不是一種新的裝載器型別,而是一種抽象的說法,它的具體表現形式為:呼叫Thread.getCurrentThread().getContextClassLoader()所返回的那個ClassLoader。它和JVM預設的類裝載器以及自定義類裝載之間是什麼關係呢?下面通過一個實驗來看一下。

 

3 實戰演練

(1)步驟一

  上圖進行了這樣一個實驗:首先一個名為Class(1)的類中啟動MainThread(其實就是這個類裡面有main函式的意思啦),注意這個類的名字後面標出了其所在的路徑(即ClassPath),然後在裡面進行測試,發現目前它的裝載器和當前執行緒(MainThread)的ContextClassLoader都是AppClassLoader。然後Class(1)啟動了一個新執行緒Class(2)。這裡的Class(2)是一個Thread的子類,執行Class(2)程式碼的執行緒我稱之為Thread-0。

(2)步驟二

  上圖可以看到Class(2)的裝載器和ContextClassLoader同樣都是AppClassLoader。隨後我在Class(2)中建立了一個新的URLCLassLoader,並用這個ClassLoader來載入另一個和Class(1)不在同一個ClassPath下的類Class(3)。此時我們就可以看到變化:即載入Class(3)的裝載器是URLClassLoader,而ContextClassLoader還仍然是AppClassLoader。

(2)步驟三

  最後我們在Class(3)中啟動了一個執行緒類Class(4),發現Class(4)也是由URLClassLoader載入的,而此時ContextClassLoader仍然是AppClassLoader。

    在整個過程中,裝載類的ClassLoader發生了變化,由於執行緒類Class(4)是由Class(3)啟動的,所以裝載它的類裝載器就變成了URLClassLoader。與此同時,所有執行緒的ContextClassLoader都繼承了生成該執行緒的ContextClassLoader--AppClassLoader。

  如果我們在第二步的結尾執行了綠色框中的程式碼:setContextClassLoader(),則結果就會變成下面這個樣子:

 

  我們可以清楚地看到,由於Thread-0將其ContextClassLoader設定成了URLClassLoader,而Thread-1是在Thread-0裡面生成的,所以就繼承了其ContextClassLoader,變成了URLClassLoader。

3 後記

  這裡列出的試驗可能不見得全面,但相信足以說明問題,應該可以說明ContextClassLoader與其它類裝載器的區別所在。但有可能ContextClassLoader還有其他的不同之處,希望有這方面經驗的朋友一起討論。

  Thread.currentThread().getContextClassLoader()的意義:

  父Classloader可以使用當前執行緒Thread.currentthread().getContextLoader()中指定的classloader中載入的類。顛覆了父ClassLoader不能使用子Classloader或者是其它沒有直接父子關係的Classloader中載入的類這種情況。這個就是Context Class Loader的意義。

五、Current ClassLoader

  當前類所屬的ClassLoader,在虛擬機器中類之間引用,預設就是使用這個ClassLoader。另外,當你使用Class.forName(), Class.getResource()這幾個不帶ClassLoader引數的方法時,默認同樣使用當前類的ClassLoader。你可以通過方法XX.class.GetClassLoader()獲取。