本文是一篇譯文。原文:Find a way out of the ClassLoader mazehtml
對於類加載器,普通Java應用開發人員不須要了解太多。但對於系統開發人員,正確理解Java的類加載器模型是開發Java系統軟件的關鍵。好久以來,我一直對ClassLoader許多問題感到很模糊,本身也在一直探討ClassLoader的機制,但苦於Java這方面的文檔太少,許多東西都是本身學習JDK源碼和看開源系統應用項目的代碼總結出來,很不清晰。前不久在幫朋友作那個企業應用平臺時,對這方面的知識深刻研究和學習了一下,遇到的最好的文檔就是這篇文章了。在這兒翻譯出來,與但願寫系統代碼的朋友分享。java
原文太長,分篇譯出。喜歡看原文的朋友不妨直接閱讀原文。web
問題:什麼時候使用Thread.getContextClassLoader()
?算法
這是一個很常見的問題,但答案卻很難回答。這個問題一般在須要動態加載類和資源的系統編程時會遇到。總的說來動態加載資源時,每每須要從三種類加載器裏選擇:系統或說程序的類加載器、當前類加載器、以及當前線程的上下文類加載器。在程序中應該使用何種類加載器呢?編程
系統類加載器一般不會使用。此類加載器處理啓動應用程序時classpath指定的類,能夠經過ClassLoader.getSystemClassLoader()來得到。全部的ClassLoader.getSystemXXX()接口也是經過這個類加載器加載的。通常不要顯式調用這些方法,應該讓其餘類加載器代理到系統類加載器上。因爲系統類加載器是JVM最後建立的類加載器,這樣代碼只會適應於簡單命令行啓動的程序。一旦代碼移植到EJB、Web應用或者Java Web Start應用程序中,程序確定不能正確執行。bootstrap
所以通常只有兩種選擇,當前類加載器和線程上下文類加載器。當前類加載器是指當前方法所在類的加載器。這個類加載器是運行時類解析使用的加載器,Class.forName(String)和Class.getResource(String)也使用該類加載器。代碼中X.class的寫法使用的類加載器也是這個類加載器。api
線程上下文類加載器在Java 2(J2SE)時引入。每一個線程都有一個關聯的上下文類加載器。若是你使用new Thread()方式生成新的線程,新線程將繼承其父線程的上下文類加載器。若是程序對線程上下文類加載器沒有任何改動的話,程序中全部的線程將都使用系統類加載器做爲上下文類加載器。Web應用和Java企業級應用中,應用服務器常常要使用複雜的類加載器結構來實現JNDI(Java命名和目錄接口)、線程池、組件熱部署等功能,所以理解這一點尤爲重要。服務器
爲何要引入線程的上下文類加載器?將它引入J2SE並非純粹的噱頭,因爲Sun沒有提供充分的文檔解釋說明這一點,這使許多開發者很糊塗。實際上,上下文類加載器爲一樣在J2SE中引入的類加載代理機制提供了後門。一般JVM中的類加載器是按照層次結構組織的,目的是每一個類加載器(除了啓動整個JVM的原初類加載器)都有一個父類加載器。當類加載請求到來時,類加載器一般首先將請求代理給父類加載器。只有當父類加載器失敗後,它才試圖按照本身的算法查找並定義當前類。框架
有時這種模式並不能老是奏效。這一般發生在JVM核心代碼必須動態加載由應用程序動態提供的資源時。拿JNDI爲例,它的核心是由JRE核心類(rt.jar)實現的。但這些核心JNDI類必須能加載由第三方廠商提供的JNDI實現。這種狀況下調用父類加載器(原初類加載器)來加載只有其子類加載器可見的類,這種代理機制就會失效。解決辦法就是讓核心JNDI類使用線程上下文類加載器,從而有效的打通類加載器層次結構,逆着代理機制的方向使用類加載器。jvm
順便提一下,XML解析API(JAXP)也是使用此種機制。當JAXP仍是J2SE擴展時,XML解析器使用當前累加載器方法來加載解析器實現。但當JAXP成爲J2SE核心代碼後,類加載機制就換成了使用線程上下文加載器,這和JNDI的緣由類似。
好了,如今咱們明白了問題的關鍵:這兩種選擇不可能適應全部狀況。一些人認爲線程上下文類加載器應成爲新的標準。但這在不一樣JVM線程共享數據來溝通時,就會使類加載器的結構亂七八糟。除非全部線程都使用同一個上下文類加載器。並且,使用當前類加載器已成爲缺省規則,它們普遍應用在類聲明、Class.forName等情景中。即便你想盡量只使用上下文類加載器,老是有這樣那樣的代碼不是你所能控制的。這些代碼都使用代理到當前類加載器的模式。混雜使用代理模式是很危險的。
更爲糟糕的是,某些應用服務器將當前類加載器和上下文類加器分別設置成不一樣的ClassLoader實例。雖然它們擁有相同的類路徑,可是它們之間並不存在父子代理關係。想一想這爲何可怕:記住加載並定義某個類的類加載器是虛擬機內部標識該類的組成部分,若是當前類加載器加載類X並接着執行它,如JNDI查找類型爲Y的數據,上下文類加載器可以加載並定義Y,這個Y的定義和當前類加載器加載的相同名稱的類就不是同一個,使用隱式類型轉換就會形成異常。
這種混亂的情況還將在Java中存在很長時間。在J2SE中還包括如下的功能使用不一樣的類加載器:
* JNDI使用線程上下文類加載器
* Class.getResource()和Class.forName()使用當前類加載器
* JAXP使用上下文類加載器
* java.util.ResourceBundle使用調用者的當前類加載器
* URL協議處理器使用java.protocol.handler.pkgs系統屬性並只使用系統類加載器。
* Java序列化API缺省使用調用者當前的類加載器
這些類加載器很是混亂,沒有在J2SE文檔中給以清晰明確的說明。
java開發人員應該怎麼作?
若是你的實現是利用特定的框架,那麼恭喜你,實現它遠比實現框架要簡單得多!例如,在web應用和EJB應用中,你僅僅只要使用 Class.getResource()就足夠了。
其餘的情形下,俺有個建議(這個原則是俺工做中發現的,侵權必究,抵制盜版。),
下面這個類能夠在整個應用中的任何地方使用,做爲一個全局的ClassLoader(全部的示例代碼能夠從download下載):
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.
You acquire a classloader reference by calling theClassLoaderResolver.getClassLoader()
static method and use the result to load classes and resources via the normal java.lang.ClassLoader
API. Alternatively, you can use this ResourceLoader
API as a drop-in replacement for java.lang.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
The decision of what constitutes the best classloader to use is factored out into a pluggable component implementing the IClassLoadStrategy
interface:
public interface IClassLoadStrategy { ClassLoader getClassLoader (ClassLoadContext ctx); } // End of interface
To help IClassLoadStrategy
make its decision, it is given a ClassLoadContext
object:
public class ClassLoadContext { public final Class getCallerClass () { return m_caller; } ClassLoadContext (final Class caller) { m_caller = caller; } private final Class m_caller; } // End of class
ClassLoadContext.getCallerClass()
returns the class whose code calls intoClassLoaderResolver
or ResourceLoader
. This is so that the strategy implementation can figure out the caller's classloader (the context loader is always available asThread.currentThread().getContextClassLoader()
). Note that the caller is determined statically; thus, my API does not require existing business methods to be augmented with extra Class
parameters and is suitable for static methods and initializers as well. You can augment this context object with other attributes that make sense in your deployment situation.
All of this should look like a familiar Strategy design pattern to you. The idea is that decisions like "always context loader" or "always current loader" get separated from the rest of your implementation logic. It is hard to know ahead of time which strategy will be the right one, and with this design, you can always change the decision later.
I have a default strategy implementation that should work correctly in 95 percent of real-life situations:
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 'callerLoader' and 'contextLoader' are in a parent-child // relationship, always choose the child: if (isChild (contextLoader, callerLoader)) result = callerLoader; else if (isChild (callerLoader, contextLoader)) result = contextLoader; else { // This else branch could be merged into the previous one, // but I show it here to emphasize the ambiguous case: result = contextLoader; } final ClassLoader systemLoader = ClassLoader.getSystemClassLoader (); // Precaution for when deployed as a bootstrap or extension class: if (isChild (result, systemLoader)) result = systemLoader; return result; } ... more methods ... } // End of class
上面的邏輯比較簡單,若是當前ClassLoader和Context ClassLoader是父子關係,那就總選兒子,根據委託原則,這個很容易理解。
若是兩人平級,選擇正確的ClassLoader很重要,運行時不容許含糊。這種狀況下,個人代碼選擇Context ClassLoader(這是俺我的的經驗之談),固然也不要擔憂不能改變,你能隨便根據須要改變。通常而言,Context ClassLoader比較適合框架,而Current ClassLoader在業務邏輯中用的更多。
最後,檢查確保選中的ClassLoader不是System ClassLoader的parent,一旦高於System ClassLoader ,請使用System ClassLoader(你的類部署在Ext路徑下面,就會出現這種狀況)。
請注意,俺故意沒關注被載入資源的名稱。Java XML API 成爲java 核心api的經歷告訴咱們,根據資源名稱過濾是很不cool的idea。並且 我也沒有去確認到底哪一個ClassLoader被取得了,由於只要清楚原理,這很容易被推理出來。(哈哈,俺是強淫)
儘管討論java 的ClassLoader不是一個很cool的話題(譯者注,當年不cool,可是如今很cool),並且Java EE的ClassLoader策略愈加的依賴各類平臺的升級。若是這沒有一個更好的設計的話,將會變成一個大大的問題。不敢您是否贊成俺的觀點,俺尊重你說話的權利,因此請給俺分享您的意見經驗。