問題:何時使用Thread.getContextClassLoader()?

這是一個很常見的問題,但答案卻很難回答。這個問題通常在需要動態加載類和資源的系統編程時會遇到。總的說來動態加載資源時,往往需要從三種類加載器裏選擇:系統或說程序的類加載器、當前類加載器、以及當前線程的上下文類加載器。在程序中應該使用何種類加載器呢? 系統類加載器通常不會使用。此類加載器處理啓動應用程序時classpath指定的類,可以通過ClassLoader.getSystemClassLoader()來獲得。所有的ClassLoader.getSystemXXX()接口也是通過這個類加載器加載的。一般不要顯式調用這些方法,應該讓其他類加載器代理到系統類加載器上。由於系統類加載器是JVM最後創建的類加載器,這樣代碼只會適應於簡單命令行啓動的程序。一旦代碼移植到EJB、Web應用或者Java Web Start應用程序中,程序肯定不能正確執行。 因此一般只有兩種選擇,當前類加載器和線程上下文類加載器。當前類加載器是指當前方法所在類的加載器。這個類加載器是運行時類解析使用的加載器,Class.forName(String)和Class.getResource(String)也使用該類加載器。代碼中X.class的寫法使用的類加載器也是這個類加載器。 線程上下文類加載器在Java 2(J2SE)時引入。每個線程都有一個關聯的上下文類加載器。如果你使用new Thread()方式生成新的線程,新線程將繼承其父線程的上下文類加載器。如果程序對線程上下文類加載器沒有任何改動的話,程序中所有的線程將都使用系統類加載器作爲上下文類加載器。Web應用和Java企業級應用中,應用服務器經常要使用複雜的類加載器結構來實現JNDI(Java命名和目錄接口)、線程池、組件熱部署等功能,因此理解這一點尤其重要。 爲什麼要引入線程的上下文類加載器?將它引入J2SE並不是純粹的噱頭,由於Sun沒有提供充分的文檔解釋說明這一點,這使許多開發者很糊塗。實際上,上下文類加載器爲同樣在J2SE中引入的類加載代理機制提供了後門。通常JVM中的類加載器是按照層次結構組織的,目的是每個類加載器(除了啓動整個JVM的原初類加載器)都有一個父類加載器。當類加載請求到來時,類加載器通常首先將請求代理給父類加載器。只有當父類加載器失敗後,它才試圖按照自己的算法查找並定義當前類。 有時這種模式並不能總是奏效。這通常發生在JVM核心代碼必須動態加載由應用程序動態提供的資源時。拿JNDI爲例,它的核心是由JRE核心類(rt.jar)實現的。但這些核心JNDI類必須能加載由第三方廠商提供的JNDI實現。這種情況下調用父類加載器(原初類加載器)來加載只有其子類加載器可見的類,這種代理機制就會失效。解決辦法就是讓核心JNDI類使用線程上下文類加載器,從而有效的打通類加載器層次結構,逆着代理機制的方向使用類加載器。 順便提一下,XML解析API(JAXP)也是使用此種機制。當JAXP還是J2SE擴展時,XML解析器使用當前累加載器方法來加載解析器實現。但當JAXP成爲J2SE核心代碼後,類加載機制就換成了使用線程上下文加載器,這和JNDI的原因相似。 好了,現在我們明白了問題的關鍵:這兩種選擇不可能適應所有情況。一些人認爲線程上下文類加載器應成爲新的標準。但這在不同JVM線程共享數據來溝通時,就會使類加載器的結構亂七八糟。除非所有線程都使用同一個上下文類加載器。而且,使用當前類加載器已成爲缺省規則,它們廣泛應用在類聲明、Class.forName等情景中。即使你想儘可能只使用上下文類加載器,總是有這樣那樣的代碼不是你所能控制的。這些代碼都使用代理到當前類加載器的模式。混雜使用代理模式是很危險的。 更爲糟糕的是,某些應用服務器將當前類加載器和上下文類加器分別設置成不同的ClassLoader實例。雖然它們擁有相同的類路徑,但是它們之間並不存在父子代理關係。想想這爲什麼可怕:記住加載並定義某個類的類加載器是虛擬機內部標識該類的組成部分,如果當前類加載器加載類X並接着執行它,如JNDI查找類型爲Y的數據,上下文類加載器能夠加載並定義Y,這個Y的定義和當前類加載器加載的相同名稱的類就不是同一個,使用隱式類型轉換就會造成異常。 這種混亂的狀況還將在Java中存在很長時間。在J2SE中還包括以下的功能使用不同的類加載器: l JNDI使用線程上下文類加載器 l Class.getResource()和Class.forName()使用當前類加載器 l JAXP使用上下文類加載器 l java.util.ResourceBundle使用調用者的當前類加載器 l URL協議處理器使用java.protocol.handler.pkgs系統屬性並只使用系統類加載器。 l Java序列化API缺省使用調用者當前的類加載器 這些類加載器非常混亂,沒有在J2SE文檔中給以清晰明確的說明。 該如何選擇類加載器? 如若代碼是限於某些特定框架,這些框架有着特定加載規則,則不要做任何改動,讓框架開發者來保證其工作(比如應用服務器提供商,儘管他們並不能總是做對)。如在Web應用和EJB中,要使用Class.gerResource來加載資源。在其他情況下,需要考慮使用下面的代碼,這是作者本人在工作中發現的經驗: public abstract class ClassLoaderResolver { /** * This method selects the best classloader instance to be used for * class/resource loading by whoever calls this method. The decision * typically involves choosing between the caller's current, thread context, * system, and other classloaders in the JVM and is made by the {@link IClassLoadStrategy} * instance established by the last call to {@link #setStrategy}. * * @return classloader to be used by the caller ['null' indicates the * primordial loader] */ public static synchronized ClassLoader getClassLoader () { final Class caller = getCallerClass (0); final ClassLoadContext ctx = new ClassLoadContext (caller); return s_strategy.getClassLoader (ctx); } public static synchronized IClassLoadStrategy getStrategy () { return s_strategy; } public static synchronized IClassLoadStrategy setStrategy (final IClassLoadStrategy strategy) { final IClassLoadStrategy old = s_strategy; s_strategy = strategy; return old; } /** * A helper class to get the call context. It subclasses SecurityManager * to make getClassContext() accessible. An instance of CallerResolver * only needs to be created, not installed as an actual security * manager. */ private static final class CallerResolver extends SecurityManager { protected Class [] getClassContext () { return super.getClassContext (); } } // End of nested class /* * Indexes into the current method call context with a given * offset. */ private static Class getCallerClass (final int callerOffset) { return CALLER_RESOLVER.getClassContext () [CALL_CONTEXT_OFFSET + callerOffset]; } private static IClassLoadStrategy s_strategy; // initialized in private static final int CALL_CONTEXT_OFFSET = 3; // may need to change if this class is redesigned private static final CallerResolver CALLER_RESOLVER; // set in static { try { // This can fail if the current SecurityManager does not allow // RuntimePermission ("createSecurityManager"): CALLER_RESOLVER = new CallerResolver (); } catch (SecurityException se) { throw new RuntimeException ("ClassLoaderResolver: could not create CallerResolver: " + se); } s_strategy = new DefaultClassLoadStrategy (); } } // End of class. 可通過調用ClassLoaderResolver.getClassLoader()方法來獲取類加載器對象,並使用其ClassLoader的接口來加載類和資源。此外還可使用下面的ResourceLoader接口來取代ClassLoader接口: public abstract class ResourceLoader { /** * @see java.lang.ClassLoader#loadClass(java.lang.String) */ public static Class loadClass (final String name) throws ClassNotFoundException { final ClassLoader loader = ClassLoaderResolver.getClassLoader (1); return Class.forName (name, false, loader); } /** * @see java.lang.ClassLoader#getResource(java.lang.String) */ public static URL getResource (final String name) { final ClassLoader loader = ClassLoaderResolver.getClassLoader (1); if (loader != null) return loader.getResource (name); else return ClassLoader.getSystemResource (name); } ... more methods ... } // End of class 決定應該使用何種類加載器的接口是IClassLoaderStrategy

發佈了70 篇原創文章 · 獲贊 5 · 訪問量 47萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章