来自:http://www.javaworld.com/article/2077344/core-java/find-a-way-out-of-the-classloader-maze.html
问题:应该在什么时候使用线程上下文加载器?
回答:
虽然不经常被问起这个问题,但是这个问题却很难给予一个正确的答案。一般来说,这个问题都来自于框架编程过程中需要动态的加载资源。一般来说,加载一个资源的时候,你会有至少有三个ClassLoader可以用来加载资源,系统ClassLoader(AppClassLoader),当前ClassLoader(可以理解为加载了当前Class的ClassLoader,比如你编写并部署在servlet容器中的程序,它使用WebAppClassLoader)和线程上下文ContextClassLoader。如何去选择使用哪个ClassLoader呢?
首先因该被排除掉的应该是系统ClassLoader,这个就是main的入口,通过载入-classpath的资源来加载类。
这段代码从sun.misc.Launcher.AppClassLoader中摘抄:
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException
{
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}
其实AppClassLoader继承了URLClassLoader,它所做的工作,就是将java.class.path下的资源,转换为URL,然后加入到AppClassLoader中,除此没有别的特殊的地方。
限定在Java9之前的版本
能够通过静态方法ClassLoader.getSystemClassLoader()
来获取到这个AppClassLoader。一般来说非常少的需求,需要获取到AppClassLoader,然后用它来加载一个类,因为都会使用其他的ClassLoader来加载类,并通过委派的方式到达AppClassLoader。
如果你编写的程序运行在最后一个ClassLoader是AppClassLoader的情况下,那么你的程序就只能在命令行下运行,因为你的程序需要依赖均在classpath下设置好,而如果将程序直接部署在WebApp容器中,那么肯定会出问题。
接下来,只有两个选择了,当前ClassLoader和线程上下文ClassLoader,以下简称:CurrentClassLoader和ContextClassLoader。
CurrentClassLoader是当前方法所属的Class,加载这个Class的ClassLoader,这样有些别扭,其实就是如果A类中有方法调用,在方法调用中用到了B,那么加载B的ClassLoader一定是加载A的ClassLoader,那么在加载B的时候,用来加载B的ClassLoader就是CurrentClassLoader。
这里简单介绍一下这个仿佛看不到的CurrentClassLoader是如何出现的。
在如下这段代码中:
class A {
public void m() {
B b = new B();
}
}
B是如何加载的呢?其实等值于A.class.getClassLoader().loadClass(“B”);
通过这种方式获取到B的类型。
那么如果是这段代码:
class A {
public void m() {
Class<?> clazz = B.class;
}
}
相当于Class.forName(“B”)
,而Class.forName
进入方法后,后续的Class载入会利用Class.class.getClassLoader().loadClass(“B”)
,也就是利用bootstrap来载入B,但是事实上还是利用载入A的ClassLoader,也就是CurrentClassLoader来载入B,看一下Class.forName
的实现:
public static Class<?> forName(String className)
throws ClassNotFoundException {
return forName0(className, true,
ClassLoader.getClassLoader(Reflection.getCallerClass()));
}
Class.forName
运作时,通过Reflection.getCallerClass()
,能够获取是谁调用了Class.forName
,这时Reflection.getClassClass()
返回的就是A.class,这样在通过指定ClassLoader来载入B,就符合原有含义了。可以通过观察,通过Java的rt.jar中的API,返回给客户端时,都使用了获取调用者的ClassLoader的特性,因为在rt.jar中,是无法找到自定义类型的。
通过Reflection.getCallerClass()
可以获取到调用Class.forName
的类的ClassLoader,从而虽然中间涉及到了bootstrap加载的类(Class),但是依旧能够维护“当前”这个语义。
Java自身除了通过Reflection.getCallerClass
来获取调用的类的类型,在deSerialization中也需要知道类型的信息。在序列化后的内容中,已经包含了当前用户自定义类的类型信息,那么如何在ObjectInputStream调用中,能够拿到客户端的类型呢?通过调用Class.forName
?肯定不可以,因为在ObjectInputStream中调用这个,会使用bootstrap来加载,那么它肯定加载不到所需要的类。
答案是通过查询栈信息,通过sun.misc.VM.latestUserDefinedLoader();
获取从栈上开始计算,第一个不为空(bootstrap classloader是空)的ClassLoader便返回。
可以试想,在ObjectInputStream运作中,通过直接获取当前调用栈中,第一个非空的ClassLoader,这种做法能够非常便捷的定位用户的ClassLoader,也就是用户在进行:
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(“xx.dat”));
B b = (B) ois.readObject();
这种调用的时候,依旧能够通过“当前”的ClassLoader正确的加载用户的类。
可以说Reflection.getCallerClass和sun.misc.VM.latestuserDefinedLoader都是用来突破双亲委派模型的一种解决方式,它能让Java在bootstrap加载的代码中运行时,能够获取到外界(用户)使用的子ClassLoader。
ContextClassLoader是作为Thread的一个成员变量出现的,一个线程在构造的时候,它会从parent线程中继承这个ClassLoader,但是Java的文档中对这个ClassLoader描述非常有限,但是它对于理解JNDI以及JAXP等技术有非常大的帮助,个人认为它是应用服务器,或者框架需要特别关注的一种ClassLoader。
通过介绍CurrentClassLoader中,用来突破双亲委派模型的目的,而ContextClassLoader也是为了完成这个工作。试想:
如果一个JNDI的提供方,或者JAXP的提供方,他们的SPI是通过bootstrap加载的,但是他们的实现类必须通过应用ClassLoader甚至是更下层的ClassLoader来加载。那么在其初始化的过程中,需要考虑如果获取到部署了SPI实现的ClassLoader,而给出的方案是使用ContextClassLoader。
在javax.xml.parsers.DocumentBuilderFactory
中,进行创建SPI实现的方法:
public static DocumentBuilderFactory newInstance() {
try {
return (DocumentBuilderFactory) FactoryFinder.find(
/* The default property name according to the JAXP spec */
"javax.xml.parsers.DocumentBuilderFactory",
/* The fallback implementation class name */
"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl");
} catch (FactoryFinder.ConfigurationError e) {
throw new FactoryConfigurationError(e.getException(),
e.getMessage());
}
}
可以看到通过查询一个property的Key来定位客户端的实现者,或者在找不到时,默认使用com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl
。
而在FactoryFinder中,find方法通过如下的方式定位实现者:
String serviceId = "META-INF/services/" + factoryId;
InputStream is = null;
ClassLoader cl = ss.getContextClassLoader();
boolean useBSClsLoader = false;
if (cl != null) {
is = ss.getResourceAsStream(cl, serviceId);
if (is == null) {
cl = FactoryFinder.class.getClassLoader();
is = ss.getResourceAsStream(cl, serviceId);
useBSClsLoader = true;
}
} else {
cl = FactoryFinder.class.getClassLoader();
is = ss.getResourceAsStream(cl, serviceId);
useBSClsLoader = true;
}
可以看到cl变量,就是当前线程的ContextClassLoader,选择使用这种方式,是因为不同的部署(通过classpath启动的控制台程序,通过Webapp部署的程序)方式不同,最终都需要有一个用户ClassLoder来查找到客户端的实现,通过前面的Reflection.getCallerClass
或者sun.misc.VM
中获取最近一个不为空的ClassLoder的方式都不能很好的满足要求,那么就利用一个指定的ClassLoder来完成,也就是接口实现者能够很明确的被这个ClassLorder加载,这个选择就是ContextClassLoader。
可以看出来CurrentClassLoader对用户来说是自动的,隐式的,而ContextClassLoader需要显示的使用,先进行设置然后再进行使用。
可以参看:https://github.com/weipeng2k/jarviewer
其中提到了ResourceLoader,这个可以用来合适的加载一个Class。
如果未来Java有更多种的类加载方式,并且看到从目前的发行版中,越来越多的方式被加入,如果没有一个良好的描述和规划,那么这将成为一个严重的问题。
网友评论