翻譯:走出類加載器迷宮
走出類加載器迷宮(本人翻譯,轉載請註明出處)
http://tyrion.iteye.com/blog/1958814
系統類加載器, 當前類加載器, 上下文類加載器? 你應該用哪一個?
By Vladimir Roubtsov, JavaWorld.com, 06/06/03
June 6, 2003
Q:我什麼時候該用Thread.getContextClassLoader()?
A:這個問題雖然不常見,卻很難正確回答。它一般出現在框架編程中,作爲解決類和資源動態加載的一個好方法。總的來說,當動態加載一個資源時,至少有三種類加載器可供選擇: 系統類加載器(也被稱爲應用類加載器)(system classloader),當前類加載器(current classloader),和當前線程的上下文類加載器( the current thread context classloader)。上面提到的問題指的是最後一種加載器。哪一個類加載器是正確的?
容易排除的一個選擇:系統類加載器。這個類加載器處理classpath環境變量所指定的路徑下的類和資源,可以通過ClassLoader.getSystemClassLoader()方法以編程式訪問。所有的ClassLoader.getSystemXXX()API方法也是通過這個類加載器訪問。你應該很少寫代碼來顯式調用,而是以其它的類加載器委託給系統類加載器來代替。否則,當系統類加載器是JVM創建的最後一個類加載器時你的代碼將只能工作在簡單的命令行應用中。只要你把代碼遷移到EJB,web應用,或Java Web Start應用中肯定會出問題。
所以,現在我們是兩個選擇:當前類加載器和上下文類加載器。根據定義,當前類加載器加載和定義當前方法所屬的那個類。這個類加載器在你使用帶單個參數的Class.forName()方法,Class.getResource()方法和相似方法時會在運行時類的鏈接過程中被隱式調用。它也出現在像X.class語法的字母調用中。(參見"Get a Load of That Name!"獲取詳細信息)
線程上下文類加載器是在J2SE中被引進的。每一個線程分配一個上下文類加載器(除非線程由本地代碼創建)。該加載器是通過Thread.setContextClassLoader()方法來設置。如果你在線程構造後不調用這個方法,這個線程將會從它的父線程(譯者注:這裏的父線程是指執行創建新線程對象語句的線程)中繼承上下文類加載器。如果你在整個應用中不做任何設置,所有線程將以系統類加載器作爲它們自己的上下文加載器。重要的是明白自從Web和J2EE應用服務器爲了像JNDI,線程池,組件熱部署等特性而採用複雜的類加載器層次結構後,這(譯者注:指整個應用中不做任何設置)是很少見的情況。
爲什麼線程上下文類加載器在第一位?它們被介紹進J2SE時並沒有大張旗鼓。從Sun公司缺乏正確的指導和文檔或許解釋了爲什麼許多開發人員會對此概念困惑。
事實上,上下文類加載器提供了一個後門繞過在J2SE中介紹的類的加載委託機制。通常情況下,一個JVM中的所有類加載器被組織成一個層次結構,使得每一個類加載器(除了啓動整個JVM的原始類加載器)都有一個父加載器。當被要求加載一個類時,每一個類加載器都將先委託父加載器來加載,只有父加載器都不能成功加載時當前類加載器纔會加載。
有時這種加載順序不能正常工作,通常發生在有些JVM核心代碼必須動態加載由應用程序開發人員提供的資源時。以JNDI舉例:它的核心內容(從J2SE1.3開始)在rt.jar中的引導類中實現了,但是這些JNDI核心類可能加載由獨立廠商實現和部署在應用程序的classpath中的JNDI提供者。這個場景要求一個父類加載器(這個例子中的原始類加載器,即加載rt.jar的加載器)去加載一個在它的子類加載器(系統類加載器)中可見的類。此時通常的J2SE委託機制不能工作,解決辦法是讓JNDI核心類使用線程上下文加載器,從而有效建立一條與類加載器層次結構相反方向的“通道”達到正確的委託。
另外,上段可能提醒你別的事情:用作XML解析的Java API(JAXP)。是的,當JAXP只是J2SE的擴展時,XML解析工廠使用當前類加載器作爲啓動解析器的實現。當JAXP作爲J2SE1.4核心的一部分時,類加載改爲使用線程上下文類加載器,JNDI情況完全類似(使很多程序員困惑)。明白我說缺少來自Sun的指導的意思了嗎?
在這些介紹後,來看看問題的癥結:剩下的兩個選擇都不是在任何情況下都正確的。有人認爲線程類加載器應該編程新的標準方案。然而,如果多個JVM線程通過共享數據通信時這將造成一個非常混亂的類加載圖景,除非他們都使用同一個上下文加載器實例。還有,委託給當前類加載器已經是一箇舊規則存在於像class字面調用(即X.class)或顯式調用Class.forName()的情況中(這是爲什麼,順便說下,我建議避免使用這個方法的帶有一個參數的版本)。即使你做出努力盡最大程度明確只使用上下文加載器,總是會有一些代碼不在你的控制之下而是委託給當前加載器。這種不受控制的混合委託策略聽起來相當危險。
更糟糕的是,某些應用服務器設置上下文和當前類加載器爲不同的加載器實例,使得有相同類路徑但卻沒有委派機制中的父子關係。花一秒鐘想想爲什麼這是特別可怕的。記住類加載器加載和定義一個類會有一個JVM內部的ID。如果當前類加載器加載一個類X,然後要執行一個JNDI查找Y類的某些信息,上下文類加載器可能加載Y類。這個Y類實例將不同於在當前類加載器中同名並可見的類實例。強行類型轉換時將會出現加載違反約束異常。
這種混亂將可能在Java中繼續存在一段時間。拿任意一個帶有任何形式的動態資源加載的J2SE API,並試着猜猜使用哪個加載策略。這裏是一個樣例:
- JNDI使用上下文類加載器
- Class.getResource()和 Class.forName() 使用當前類加載器
- JAXP 使用上下文類加載器 (截至 J2SE 1.4)
- java.util.ResourceBundle 使用調用的當前類加載器
- 通過java.protocol.handler.pkgs系統屬性指定的URL協議處理器只在引導類加載器和系統類加載器中查詢
- Java序列化API缺省使用調用者的當前類加載器
這些類和資源加載策略肯定是J2SE中的最不良記錄。
一個Java程序員要做什麼?
如果你的實現被限定於一個確定的有明確資源加載規則的框架,堅持他們。我們希望,使它們工作的負擔在實現框架的人上(如應用服務器廠商,儘管他們並不總是正確的)。例如,在一個Web應用或EJB中,只要使用Class.getResource()。
在別的情況下,你可能會考慮使用一個解決方案,我發現在個人工作中很有用。下面的類作爲一個全局決策點,用於獲取應用程序任何給定時間中最佳的類加載器(所有的示例代碼可以從download下載):
- public abstract class ClassLoaderResolver
- {
- /**
- * 這個方法提供給調用此方法的人選擇用於類/資源加載的最佳類加載器的實例。
- * 通常涉及JVM中調用者當前類加載器、線程上下文類加載器、系統類加載器和其他類
- * 加載器之間的選擇。該加載器實例由setStrategy方法設置的IClassLoadStrategy的
- * 實例提供。
- *
- * @返回類加載器實例給調用者 [返回null表示JVM的啓動類加載器]
- */
- 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;
- }
- /**
- * 一個獲取調用者上下文的幫助類。getClassContext()方法對
- * SecurityManager子類可見。只需要創建一個CallerResolver類的實例
- * 不必安裝一個實際的安全管理器
- */
- private static final class CallerResolver extends SecurityManager
- {
- protected Class [] getClassContext ()
- {
- return super.getClassContext ();
- }
- } // 嵌套類結束
- /*
- * 獲取指定偏移量位置的當前方法調用者上下文
- */
- private static Class getCallerClass (final int callerOffset)
- {
- return CALLER_RESOLVER.getClassContext () [CALL_CONTEXT_OFFSET +
- callerOffset];
- }
- private static IClassLoadStrategy s_strategy; //類裝載時初始化(見下面的靜態語句塊)
- private static final int CALL_CONTEXT_OFFSET = 3; // 如果這個類重新設計時可能需要改變這個值
- private static final CallerResolver CALLER_RESOLVER; // 類裝載時初始化(見下面的靜態語句塊)
- static
- {
- try
- {
- //如果當前安全管理器沒有("createSecurityManager")運行時權限則可能會失敗:
- CALLER_RESOLVER = new CallerResolver ();
- }
- catch (SecurityException se)
- {
- throw new RuntimeException ("ClassLoaderResolver: could not create CallerResolver: " + se);
- }
- s_strategy = new DefaultClassLoadStrategy ();
- }
- } // 類定義結束
通過ClassLoaderResolver.getClassLoader()靜態方法獲得一個類加載器的引用,可以用這個結果通過一般的類加載器API加載類和資源。另外,你可以用ResourceLoader作爲類加載器的簡易替換:
- 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 ...
- } // 類定義結束
決定使用何種類加載器的策略由IClassLoadStrategy 接口實現的,這是一個可插拔的組件:
- public interface IClassLoadStrategy
- {
- ClassLoader getClassLoader (ClassLoadContext ctx);
- } // 接口定義結束
爲了幫助IClassLoadStrategy 做決定,需要傳入一個ClassLoadContext 對象:
- public class ClassLoadContext
- {
- public final Class getCallerClass ()
- {
- return m_caller;
- }
- ClassLoadContext (final Class caller)
- {
- m_caller = caller;
- }
- private final Class m_caller;
- } // 類定義結束
ClassLoadContext.getCallerClass()返回類給ClassLoaderResolver或ResourceLoader使用。以便實現策略可以返回調用者的類加載器(上下文加載器總是可以通過Thread.currentThread().getContextClassLoader()來獲取)。需要注意的是調用者是不可變的,因此,我的API不需要現有業務方法增加額外的Class 參數,同樣也可用於靜態方法和初始化方法。你可以根據你的部署情況添加其它屬性擴展這個context對象。
所有這些看起像設計模式中的策略模式。核心思想是將“使用上下文類加載器”和“使用當前類加載器”的決策同你的其它具體實現邏輯分開。我們很難提前預知哪個策略是正確的,這種設計,你可以隨時改變策略。
我有一個默認策略實現可以在現實工作95%的情況下正確工作:
- public class DefaultClassLoadStrategy implements IClassLoadStrategy
- {
- public ClassLoader getClassLoader (final ClassLoadContext ctx)
- {
- final ClassLoader callerLoader = ctx.getCallerClass ().getClassLoader ();
- final ClassLoader contextLoader = Thread.currentThread ().getContextClassLoader ();
- ClassLoader result;
- // 如果調用者加載器和上下文加載器是父子關係,則一直選擇子加載器:
- if (isChild (contextLoader, callerLoader))
- result = callerLoader;
- else if (isChild (callerLoader, contextLoader))
- result = contextLoader;
- else
- {
- // else分支可以被合併到前一個,單獨列出來是要強調在模棱兩可的情況下:
- result = contextLoader;
- }
- final ClassLoader systemLoader = ClassLoader.getSystemClassLoader ();
- // 部署時作爲啓動類或啓動擴展類的注意事項:
- if (isChild (result, systemLoader))
- result = systemLoader;
- return result;
- }
- ... more methods ...
- } // 類定義結束
上面的邏輯理解起來很簡單。如果調用者當前加載器和上下文加載器是父子關係,則一直選擇子類加載器。子類加載器可見的資源通常也是父類加載器可見的,只要遵循J2SE的代理委託規則,大部分情況下就是正確的策略。
當前加載器和上下文加載器不是父子關係時不可能給出正確的策略。理想情況下,Java運行時不應該允許這種模棱兩可的狀況。一旦出現這種情形,我的代碼就選擇上下文加載器:這個策略基於本人大部分時間正確工作的經驗。你可以根據需要修改代碼。上下文加載器可能是框架組件中更好的選擇,當前加載器可能是業務邏輯中更好的選擇。
最後,一個簡單的檢查保證所選的類加載器不是系統類加載器的父加載器。如果你正在編寫的代碼可能部署爲標準擴展庫時這是個好習慣。
請注意我故意沒檢查資源或被加載的類的名稱。如果不出意外,將變成J2SE核心的一部分的Java XML API的經驗告訴你根據類名過濾不是一個好主意。我也沒試驗類的加載看看哪個加載器先成功加載。從根本上說檢查類加載器的父子關係是一個更好和更可預測的方法。
雖然Java資源加載仍然是一個深奧的話題,隨着版本的升級J2SE越來越多的依賴於各種加載策略。如果這塊不給出一些有顯著改進的設計方案Java將有很大的麻煩。不管你贊同還是不贊同,非常感謝你的反饋和來自個人設計經驗的指正。
關於作者
Vladimir Roubtsov擁有超過13年的各種語言編程經驗,1995年開始使用Java。現在,他作爲Trilogy公司的高級工程師開發企業應用軟件。