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

http://www.cnblogs.com/549294286/p/3714692.html


實際上,在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()獲取。

 

Reference

淺析Context Class Loader

ContextClassLoader淺析

https://www.ibm.com/developerworks/cn/java/j-lo-classloader/

http://blog.sina.com.cn/s/blog_605f5b4f01010i48.html

 http://my.oschina.net/u/571166/blog/212903



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