本文是一篇譯文。原文:Find a way out of the ClassLoader maze
對于類加載器,普通Java應(yīng)用開發(fā)人員不需要了解太多。但對于系統(tǒng)開發(fā)人員,正確理解Java的類加載器模型是開發(fā)Java系統(tǒng)軟件的關(guān)鍵。很久以來,我一直對ClassLoader許多問題感到很模糊,自己也在一直探討ClassLoader的機制,但苦于Java這方面的文檔太少,許多東西都是自己學(xué)習(xí)JDK源碼和看開源系統(tǒng)應(yīng)用項目的代碼總結(jié)出來,很不清晰。前不久在幫朋友做那個企業(yè)應(yīng)用平臺時,對這方面的知識深入研究和學(xué)習(xí)了一下,遇到的最好的文檔就是這篇文章了。在這兒翻譯出來,與希望寫系統(tǒng)代碼的朋友分享。
原文太長,分篇譯出。喜歡看原文的朋友不妨直接閱讀原文。
問題:何時使用Thread.getContextClassLoader()?
這是一個很常見的問題,但答案卻很難回答。這個問題通常在需要動態(tài)加載類和資源的系統(tǒng)編程時會遇到。總的說來動態(tài)加載資源時,往往需要從三種類加載器里選擇:系統(tǒng)或說程序的類加載器、當(dāng)前類加載器、以及當(dāng)前線程的上下文類加載器。在程序中應(yīng)該使用何種類加載器呢?
系統(tǒng)類加載器通常不會使用。此類加載器處理啟動應(yīng)用程序時classpath指定的類,可以通過ClassLoader.getSystemClassLoader()來獲得。所有的ClassLoader.getSystemXXX()接口也是通過這個類加載器加載的。一般不要顯式調(diào)用這些方法,應(yīng)該讓其他類加載器代理到系統(tǒng)類加載器上。由于系統(tǒng)類加載器是JVM最后創(chuàng)建的類加載器,這樣代碼只會適應(yīng)于簡單命令行啟動的程序。一旦代碼移植到EJB、Web應(yīng)用或者Java Web Start應(yīng)用程序中,程序肯定不能正確執(zhí)行。
因此一般只有兩種選擇,當(dāng)前類加載器和線程上下文類加載器。當(dāng)前類加載器是指當(dāng)前方法所在類的加載器。這個類加載器是運行時類解析使用的加載器,Class.forName(String)和Class.getResource(String)也使用該類加載器。代碼中X.class的寫法使用的類加載器也是這個類加載器。
線程上下文類加載器在Java 2(J2SE)時引入。每個線程都有一個關(guān)聯(lián)的上下文類加載器。如果你使用new Thread()方式生成新的線程,新線程將繼承其父線程的上下文類加載器。如果程序?qū)€程上下文類加載器沒有任何改動的話,程序中所有的線程將都使用系統(tǒng)類加載器作為上下文類加載器。Web應(yīng)用和Java企業(yè)級應(yīng)用中,應(yīng)用服務(wù)器經(jīng)常要使用復(fù)雜的類加載器結(jié)構(gòu)來實現(xiàn)JNDI(Java命名和目錄接口)、線程池、組件熱部署等功能,因此理解這一點尤其重要。
為什么要引入線程的上下文類加載器?將它引入J2SE并不是純粹的噱頭,由于Sun沒有提供充分的文檔解釋說明這一點,這使許多開發(fā)者很糊涂。實際上,上下文類加載器為同樣在J2SE中引入的類加載代理機制提供了后門。通常JVM中的類加載器是按照層次結(jié)構(gòu)組織的,目的是每個類加載器(除了啟動整個JVM的原初類加載器)都有一個父類加載器。當(dāng)類加載請求到來時,類加載器通常首先將請求代理給父類加載器。只有當(dāng)父類加載器失敗后,它才試圖按照自己的算法查找并定義當(dāng)前類。
有時這種模式并不能總是奏效。這通常發(fā)生在JVM核心代碼必須動態(tài)加載由應(yīng)用程序動態(tài)提供的資源時。拿JNDI為例,它的核心是由JRE核心類(rt.jar)實現(xiàn)的。但這些核心JNDI類必須能加載由第三方廠商提供的JNDI實現(xiàn)。這種情況下調(diào)用父類加載器(原初類加載器)來加載只有其子類加載器可見的類,這種代理機制就會失效。解決辦法就是讓核心JNDI類使用線程上下文類加載器,從而有效的打通類加載器層次結(jié)構(gòu),逆著代理機制的方向使用類加載器。
順便提一下,XML解析API(JAXP)也是使用此種機制。當(dāng)JAXP還是J2SE擴展時,XML解析器使用當(dāng)前累加載器方法來加載解析器實現(xiàn)。但當(dāng)JAXP成為J2SE核心代碼后,類加載機制就換成了使用線程上下文加載器,這和JNDI的原因相似。
好了,現(xiàn)在我們明白了問題的關(guān)鍵:這兩種選擇不可能適應(yīng)所有情況。一些人認(rèn)為線程上下文類加載器應(yīng)成為新的標(biāo)準(zhǔn)。但這在不同JVM線程共享數(shù)據(jù)來溝通時,就會使類加載器的結(jié)構(gòu)亂七八糟。除非所有線程都使用同一個上下文類加載器。而且,使用當(dāng)前類加載器已成為缺省規(guī)則,它們廣泛應(yīng)用在類聲明、Class.forName等情景中。即使你想盡可能只使用上下文類加載器,總是有這樣那樣的代碼不是你所能控制的。這些代碼都使用代理到當(dāng)前類加載器的模式。混雜使用代理模式是很危險的。
更為糟糕的是,某些應(yīng)用服務(wù)器將當(dāng)前類加載器和上下文類加器分別設(shè)置成不同的ClassLoader實例。雖然它們擁有相同的類路徑,但是它們之間并不存在父子代理關(guān)系。想想這為什么可怕:記住加載并定義某個類的類加載器是虛擬機內(nèi)部標(biāo)識該類的組成部分,如果當(dāng)前類加載器加載類X并接著執(zhí)行它,如JNDI查找類型為Y的數(shù)據(jù),上下文類加載器能夠加載并定義Y,這個Y的定義和當(dāng)前類加載器加載的相同名稱的類就不是同一個,使用隱式類型轉(zhuǎn)換就會造成異常。
這種混亂的狀況還將在Java中存在很長時間。在J2SE中還包括以下的功能使用不同的類加載器:
* JNDI使用線程上下文類加載器
* Class.getResource()和Class.forName()使用當(dāng)前類加載器
* JAXP使用上下文類加載器
* java.util.ResourceBundle使用調(diào)用者的當(dāng)前類加載器
* URL協(xié)議處理器使用java.protocol.handler.pkgs系統(tǒng)屬性并只使用系統(tǒng)類加載器。
* Java序列化API缺省使用調(diào)用者當(dāng)前的類加載器
這些類加載器非常混亂,沒有在J2SE文檔中給以清晰明確的說明。
對于類加載器,普通Java應(yīng)用開發(fā)人員不需要了解太多。但對于系統(tǒng)開發(fā)人員,正確理解Java的類加載器模型是開發(fā)Java系統(tǒng)軟件的關(guān)鍵。很久以來,我一直對ClassLoader許多問題感到很模糊,自己也在一直探討ClassLoader的機制,但苦于Java這方面的文檔太少,許多東西都是自己學(xué)習(xí)JDK源碼和看開源系統(tǒng)應(yīng)用項目的代碼總結(jié)出來,很不清晰。前不久在幫朋友做那個企業(yè)應(yīng)用平臺時,對這方面的知識深入研究和學(xué)習(xí)了一下,遇到的最好的文檔就是這篇文章了。在這兒翻譯出來,與希望寫系統(tǒng)代碼的朋友分享。
原文太長,分篇譯出。喜歡看原文的朋友不妨直接閱讀原文。
問題:何時使用Thread.getContextClassLoader()?
這是一個很常見的問題,但答案卻很難回答。這個問題通常在需要動態(tài)加載類和資源的系統(tǒng)編程時會遇到。總的說來動態(tài)加載資源時,往往需要從三種類加載器里選擇:系統(tǒng)或說程序的類加載器、當(dāng)前類加載器、以及當(dāng)前線程的上下文類加載器。在程序中應(yīng)該使用何種類加載器呢?
系統(tǒng)類加載器通常不會使用。此類加載器處理啟動應(yīng)用程序時classpath指定的類,可以通過ClassLoader.getSystemClassLoader()來獲得。所有的ClassLoader.getSystemXXX()接口也是通過這個類加載器加載的。一般不要顯式調(diào)用這些方法,應(yīng)該讓其他類加載器代理到系統(tǒng)類加載器上。由于系統(tǒng)類加載器是JVM最后創(chuàng)建的類加載器,這樣代碼只會適應(yīng)于簡單命令行啟動的程序。一旦代碼移植到EJB、Web應(yīng)用或者Java Web Start應(yīng)用程序中,程序肯定不能正確執(zhí)行。
因此一般只有兩種選擇,當(dāng)前類加載器和線程上下文類加載器。當(dāng)前類加載器是指當(dāng)前方法所在類的加載器。這個類加載器是運行時類解析使用的加載器,Class.forName(String)和Class.getResource(String)也使用該類加載器。代碼中X.class的寫法使用的類加載器也是這個類加載器。
線程上下文類加載器在Java 2(J2SE)時引入。每個線程都有一個關(guān)聯(lián)的上下文類加載器。如果你使用new Thread()方式生成新的線程,新線程將繼承其父線程的上下文類加載器。如果程序?qū)€程上下文類加載器沒有任何改動的話,程序中所有的線程將都使用系統(tǒng)類加載器作為上下文類加載器。Web應(yīng)用和Java企業(yè)級應(yīng)用中,應(yīng)用服務(wù)器經(jīng)常要使用復(fù)雜的類加載器結(jié)構(gòu)來實現(xiàn)JNDI(Java命名和目錄接口)、線程池、組件熱部署等功能,因此理解這一點尤其重要。
為什么要引入線程的上下文類加載器?將它引入J2SE并不是純粹的噱頭,由于Sun沒有提供充分的文檔解釋說明這一點,這使許多開發(fā)者很糊涂。實際上,上下文類加載器為同樣在J2SE中引入的類加載代理機制提供了后門。通常JVM中的類加載器是按照層次結(jié)構(gòu)組織的,目的是每個類加載器(除了啟動整個JVM的原初類加載器)都有一個父類加載器。當(dāng)類加載請求到來時,類加載器通常首先將請求代理給父類加載器。只有當(dāng)父類加載器失敗后,它才試圖按照自己的算法查找并定義當(dāng)前類。
有時這種模式并不能總是奏效。這通常發(fā)生在JVM核心代碼必須動態(tài)加載由應(yīng)用程序動態(tài)提供的資源時。拿JNDI為例,它的核心是由JRE核心類(rt.jar)實現(xiàn)的。但這些核心JNDI類必須能加載由第三方廠商提供的JNDI實現(xiàn)。這種情況下調(diào)用父類加載器(原初類加載器)來加載只有其子類加載器可見的類,這種代理機制就會失效。解決辦法就是讓核心JNDI類使用線程上下文類加載器,從而有效的打通類加載器層次結(jié)構(gòu),逆著代理機制的方向使用類加載器。
順便提一下,XML解析API(JAXP)也是使用此種機制。當(dāng)JAXP還是J2SE擴展時,XML解析器使用當(dāng)前累加載器方法來加載解析器實現(xiàn)。但當(dāng)JAXP成為J2SE核心代碼后,類加載機制就換成了使用線程上下文加載器,這和JNDI的原因相似。
好了,現(xiàn)在我們明白了問題的關(guān)鍵:這兩種選擇不可能適應(yīng)所有情況。一些人認(rèn)為線程上下文類加載器應(yīng)成為新的標(biāo)準(zhǔn)。但這在不同JVM線程共享數(shù)據(jù)來溝通時,就會使類加載器的結(jié)構(gòu)亂七八糟。除非所有線程都使用同一個上下文類加載器。而且,使用當(dāng)前類加載器已成為缺省規(guī)則,它們廣泛應(yīng)用在類聲明、Class.forName等情景中。即使你想盡可能只使用上下文類加載器,總是有這樣那樣的代碼不是你所能控制的。這些代碼都使用代理到當(dāng)前類加載器的模式。混雜使用代理模式是很危險的。
更為糟糕的是,某些應(yīng)用服務(wù)器將當(dāng)前類加載器和上下文類加器分別設(shè)置成不同的ClassLoader實例。雖然它們擁有相同的類路徑,但是它們之間并不存在父子代理關(guān)系。想想這為什么可怕:記住加載并定義某個類的類加載器是虛擬機內(nèi)部標(biāo)識該類的組成部分,如果當(dāng)前類加載器加載類X并接著執(zhí)行它,如JNDI查找類型為Y的數(shù)據(jù),上下文類加載器能夠加載并定義Y,這個Y的定義和當(dāng)前類加載器加載的相同名稱的類就不是同一個,使用隱式類型轉(zhuǎn)換就會造成異常。
這種混亂的狀況還將在Java中存在很長時間。在J2SE中還包括以下的功能使用不同的類加載器:
* JNDI使用線程上下文類加載器
* Class.getResource()和Class.forName()使用當(dāng)前類加載器
* JAXP使用上下文類加載器
* java.util.ResourceBundle使用調(diào)用者的當(dāng)前類加載器
* URL協(xié)議處理器使用java.protocol.handler.pkgs系統(tǒng)屬性并只使用系統(tǒng)類加載器。
* Java序列化API缺省使用調(diào)用者當(dāng)前的類加載器
這些類加載器非常混亂,沒有在J2SE文檔中給以清晰明確的說明。