本文是一篇译文。原文: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策略愈加的依赖各类平台的升级。若是这没有一个更好的设计的话,将会变成一个大大的问题。不敢您是否赞成俺的观点,俺尊重你说话的权利,因此请给俺分享您的意见经验。