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()獲取。

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM