java的ClassLoader机制真可谓最神秘的技术,在之前的文章中深入简出的介绍过。这里补充两方面的问题,第一个就是CurrentClassLoader(也称为Current ClassLoader)和ContextClassLoader,第二个就是深入的谈下Class.forName和ClassLoader.loadClass。首先,先看下面的示例代码。
//代码1
class A {
public void test1() {
B b = new B();
}
}
//代码2
class A {
public void test2() {
Class<?> clazz = B.class;
}
}
//代码3
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(“xx.dat”));
B b = (B) ois.readObject();
如果你认为三段代码B.class都是由AppClassLoader加载的,那你就大错特错了,~O(∩_∩)O哈哈~!
AppClassLoader
AppClassLoader继承了URLClassLoader,它所做的工作,就是将java.class.path下的资源,转换为URL,然后加入到AppClassLoader中,除此没有别的特殊的地方。
获取AppClassLoader可以能够通过静态方法
ClassLoader.getSystemClassLoader()
,在Java9之前的版本,一般来说几乎没有的需求!为何呢?如果你的程序直接运行AppClassLoader下,那么它只能在命令行下运行,并且需要将依赖jar都放置到默认的classpath下,而要想将程序直接部署在WebApp容器中,那么肯定是不行的。
//sun.misc.Launcher.AppClassLoader中摘抄
//main的入口,通过载入-classpath的资源来加载类
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);
}
});
}
那要在WebApp容器中运行该怎么玩的?基本上都会使用其他的ClassLoader来加载类(自定义的jar),并通过委派的方式到达AppClassLoader。
首先,据我所知加载一个资源至少有三个ClassLoader
- AppClassLoader(系统ClassLoader)
- CurrentClassLoader(可以理解为加载了当前Class的ClassLoader,比如在servlet容器中就是WebAppClassLoader)
- ContextClassLoader(线程上下文)
代码1中test1()方法里的B是如何加载的呢?
其实它等价于 A.class.getClassLoader().loadClass("B");
代码2中test2()方法里的B又是如何加载的呢?
首先,代码2等价于Class.forName(“B”);其中代码1与代码2最大的区别在于:前者是隐式类加载,后者显示类加载。而Class.forName
进入方法后,难道就是通过Class.class.getClassLoader().loadClass("B"),也就是利用bootstrap来载入B吗?
其实不然,它还是利用载入A的ClassLoader加载的,也就是CurrentClassLoader来载入B,不急继续向下看。
CurrentClassLoader究竟是什么?
方法test2中,
Reflection.getCallerClass()
返回的就是A.class,它作用是获取是谁调用了Class.forName
,这样在通过指定ClassLoader来载入B,就符合原有含义了。虽然中间涉及到了bootstrap加载的类(Class),但是依旧能够维护“当前”这个语义。
//源码中可以验证
public static Class<?> forName(String className)
throws ClassNotFoundException {
return forName0(className, true, ClassLoader.getClassLoader(Reflection.getCallerClass()));
}
代码3中序列化后的内容(B.class)也是通过调用getCallerClass
?
肯定不可以,因为在ObjectInputStream中调用这个,会使用bootstrap来加载,那么它肯定加载不到所需要的类。
通过查询虚拟机栈信息,通过sun.misc.VM.latestUserDefinedLoader();
获取从栈上开始计算,第一个不为空(bootstrap classloader是空)的ClassLoader便返回。这种调用的时候,依旧能够通过“当前”的ClassLoader正确的加载用户的类。
public class ObjectInputStream{
private static ClassLoader latestUserDefinedLoader() {
return sun.misc.VM.latestUserDefinedLoader();
}
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
String name = desc.getName();
try {
return Class.forName(name, false, latestUserDefinedLoader());
} catch (ClassNotFoundException ex) {
Class<?> cl = primClasses.get(name);
if (cl != null) {
return cl;
} else {
throw ex;
}
}
}
}
小结
Reflection.getCallerClass和sun.misc.VM.latestuserDefinedLoader都是用来突破双亲委派模型的一种解决方式,它能让Java在bootstrap加载的代码中运行时,能够获取到外界(用户)使用的子ClassLoader。
再谈ContextClassLoader
ContextClassLoader是作为Thread的一个成员变量出现的,一个线程在构造的时候,它会从parent线程中继承这ClassLoader,但是Java的文档中对这个ClassLoader描述非常有限,但是它对于理解JNDI以及JAXP等技术有非常大的帮助,它是应用服务器或者框架需要特别关注的一种ClassLoader。
何时产生?
首先 contextClassLoader 是那种需要显示使用的类加载器!如果你没有显示使用它,也就永远不会在任何地方用到它,这意味着如果你使用 forName(string name) 方法加载目标类,它不会自动使用 contextClassLoader。
其次线程的 contextClassLoader 默认是从父线程那里继承过来的,所谓父线程就是创建了当前线程的线程。程序启动时的 main 线程的 contextClassLoader 就是 AppClassLoader(如果我们不去定制 contextClassLoader,它默认就是AppClassLoader,所有的类都将会是共享的)。这意味着如果没有人工去设置,那么所有的线程的 contextClassLoader 都是 AppClassLoader。
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());
}
}
//而在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.latestuserDefinedLoader中获取最近一个不为空的ClassLoder的方式都不能很好的满足要求,那么就利用一个指定的ClassLoder来完成,也就是接口实现者能够很明确的被这个ClassLorder加载,这个选择就是ContextClassLoader。
ContextClassLoader还有哪些拓展?
它可以做到跨线程共享类,父子线程之间会自动传递 contextClassLoader,所以共享起来将是自动化的。
如果不同的线程使用不同的 contextClassLoader,那么不同的线程使用的类就可以隔离开来。如果我们对业务进行划分,不同的业务使用不同的线程池,线程池内部共享同一个 contextClassLoader,线程池之间使用不同的contextClassLoader,就可以很好的起到隔离保护的作用,避免类版本冲突。
总结
可以看出来CurrentClassLoader对用户来说是自动的,隐式的,而ContextClassLoader需要显示的使用,先进行设置然后再进行使用。