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

  1. AppClassLoader(系统ClassLoader)
  2. CurrentClassLoader(可以理解为加载了当前Class的ClassLoader,比如在servlet容器中就是WebAppClassLoader)
  3. 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需要显示的使用,先进行设置然后再进行使用。