类加载器ClassLoader(二):外传

正传中我们提到,java的类加载有两个主要的特征

  1. 树形层次结构
  2. 父辈委派加载

它们就像基石一样支撑着java的整栋建筑。然而现实总是在变化,新的需求、应用随着时间的推移不断的被提出来,我们常常发现旧的机制往往给新生事物的发展带来许多的掣肘。这时候,对旧制度的“破坏”,或者说是改革、创新就必然会产生。下面就说说上面两个基础怎么被打破了。

一、打破父辈委派加载

先从第二点开始说。在正传中已经提到过,父辈委派模型只是一个建议性的机制,这本身就给打破这一机制提供了基础条件。进一步的,现实世界的应用要求也催生了开发者迈出这一步。我们从两个例子具体看一下这方面的应用。

(1)线程上下文类加载器 Thread Context ClassLoader

从名称上看,似乎线程上下文类加载器是一种新的加载器类型,然而实际上,更应该把它理解为一种类加载机制。它的作用主要是打破父辈委派加载,实现让父加载器可以请求子加载器去完成一些类的加载

为什么会引入这样的机制?这是由java中接口定义类所在位置和实现类所在位置不同造成的。

举个例子,JNDI是java中的标准服务,其代码由Bootstrap Class Loader加载。然而这里加载的许多interface,其实现类是由厂商实现的JNDI接口提供者SPI(service provider interface),并部署在应用的classpath下,如果按照正统的父辈委派模型,Bootstrap Class Loader是找不到这些SPI实现代码的。

为了解决这个问题,java引入了一个机制,就是线程上下文类加载器Thread Context Class Loader。简单的说,就是把某一个具体的ClassLoader实例对象通过java.lang.Thread的setContextClassLoader()方法绑定到线程上。

线程绑定了ClassLoader为什么就可以实现父加载器委托子加载器去加载了呢?

具体来说,在绑定ClassLoader到线程时,如果没有显示设置,系统默认的把System Class Loader的实例用于绑定。这样JNDI通过其当前线程,可以获取到这个System Class Loader,然后就可以通过它,去load需要加载的SPI代码了。基本上,java中的SPI都是这样加载的,例如JNDI、JDBC、JCE、JAXB、JBI等等。

还有一个例子也是利用到了线程上下文类加载器的,这就是spring-framework。在tomcat这种web容器中,会提供一个目录可以放置各个webapp公共用到类库。这部分类库是由tomcat自定义的一个class loader实现加载的,它处于加载各个webapp应用的class loader的父级(关于tomcat中的类加载器,具体在下一节介绍)。这时,如果把spring的jar包统一放到公共类库目录中,那么spring在加载用户自己的class时,就会发生父加载器无法找到webapp下的class的问题。

而spring给出的解决办法就是利用了Thread Contex Class Loader。下面的代码来自spring-core包中的org.springframework.util.ClassUtils类,getDefaultClassLoader方法最终会被spring创建bean实例时候使用。

public static ClassLoader getDefaultClassLoader() {
        ClassLoader cl = null;
        try {
            cl = Thread.currentThread().getContextClassLoader();
        }
        catch (Throwable ex) {
            // Cannot access thread context ClassLoader - falling back...
        }
        if (cl == null) {
            // No thread context class loader -> use class loader of this class.
            cl = ClassUtils.class.getClassLoader();
            if (cl == null) {
                // getClassLoader() returning null indicates the bootstrap ClassLoader
                try {
                    cl = ClassLoader.getSystemClassLoader();
                }
                catch (Throwable ex) {
                    // Cannot access system ClassLoader - oh well, maybe the caller can live with null...
                }
            }
        }
        return cl;
    }

从这里可以看出,spring首先尝试获取线程上下文类加载器。如果失败,则返回加载spring的类加载器,还不行的话就返回System Class Loader。

再多说一点,对于web应用,spring-web包中初始化web环境下spring context的类org.springframework.web.context.ContextLoader有方法

public WebApplicationContext initWebApplicationContext(ServletContext servletContext)

该方法中的代码段

            ClassLoader ccl = Thread.currentThread().getContextClassLoader();
            if (ccl == ContextLoader.class.getClassLoader()) {
                currentContext = this.context;
            }
            else if (ccl != null) {
                currentContextPerThread.put(ccl, this.context);
            }

用于在不同的spring包放置情况下做不同的context缓存设置。在spring包部署在webapp的WEB-INF目录下时,会进入if语句,这时ccl是tomcat实现的WebappClassloader的实例。对应的,如果spring包部署在tomcat的公共目录中时会进入else语句,此时ccl仍是WebappClassloader的实例,但是ContextLoader.class.getClassLoader()则是处于WebappClassloader父辈的类加载器(tomcat 8以前的版本是tomcat实现的StandardClassloader,这个类加载器直接继承自java.net.URLClassloader,两者没有区别。tomcat 8中,StandardClassloader被移除,直接使用了URLClassloader)。

spring-web为什么会区分这两种情况呢?这就要看看tomcat的类加载机制的实现了。

(2)Tomcat中的类加载

从上面的Thread Context Class Loader可以看出,它对java正统的“子加载器委派父加载器”的模式进行了补充,使得“父加载器也可以委派子加载器”。

然而,也有子加载器需要优先于父加载器,首先对一些类进行加载的情况。这在tomcat的实现中得到了体现。

我们知道tomcat下允许同时存在多个webapp,各个webapp下的WEB-INF目录可以放置webapp自己使用的jar、class,并且各个webapp之间不会出现互相的冲突,这个机制的实现是由于tomcat自己实现了一个WebappClassLoader,该加载器对每一个webapp都有一个对应的实例存在,该实例只负责加载该webapp的WEB-INF下的类,这样就保证了webapp之间的隔离。

另一方面,tomcat又提供了目录使得各个webapp可以共享该目录下的类。这样就有可能出现共享目录中的类和webapp下的类重复的情况。

进一步的,tomcat的类加载器的层次结构如下所示

tomcat_classloader.png

可以看到tomcat中除了Jsp Classloader外,有四类classloader,分别是

  1. Common Classloader:
    加载的类可被tomcat server和所有webapp共同使用。
    以tomcat 5.x为例,它加载的类默认放在下面配置项指定的目录
common.loader=${catalina.home}/common/classes,${catalina.home}/common/i18n/*.jar,
          ${catalina.home}/common/endorsed/*.jar,${catalina.home}/common/lib/*.jar
  1. Server Classloader:加载的类只能被tomcat server使用
    以tomcat 5.x为例,这些类默认放在
server.loader=${catalina.home}/server/classes,${catalina.home}/server/lib/*.jar
  1. Shared Classloader:加载的类能被所有webapp共享使用
    以tomcat 5.x为例,这些类放在
shared.loader=${catalina.base}/shared/classes,${catalina.base}/shared/lib/*.jar
  1. Webapp Classloader:加载的类只能被本webapp使用
    它加载的是本webapp下WEB-INF/lib和WEB-INF/classes目录下的类

如果按照传统的父辈委派加载,那么如果出现上述相同类同时放置在WEB-INF和共享目录(common、shared)的情况时就会出现共享目录的类首先被加载的情况,这不是tomcat所希望的结果。

因此,tomcat在实现WebappClassLoader时,要求它首先加载其对应webapp下WEB-INF下的类,这样处于子加载其地位的WebappClassloader就优先于处于父加载器地位的Common、Shared加载器执行了类加载动作。这就打破了父辈委派模型。当然这里WebappClassloader任然保证了java核心库会委派给父辈的Bootstrap Class Loader进行加载。

另外顺带说明两个事。

一是,上面的Common、Server、Shared Class Loader只是根据tomcat中加载不同位置下的类,对它们对应加载器以该类型名称作为命名的一个叫法。实际上这些加载器都是用同一个java类实现的。在上一节中与spring-web相关的部分已经提到过,它们三个的实际java类在tomcat 8以前的版本是tomcat自己实现的StandardClassLoader,这个类加载器直接继承自java.net.URLClassloader,两者没有区别。tomcat 8开始废弃了StandardClassLoader,改为直接使用java.net.URLClassLoader了。这三个类加载器的初始化发生在tomcat的org.apache.catalina.startup.Bootstrap类中,具体方法是

private void initClassLoaders() {
        try {
            commonLoader = createClassLoader("common", null);
            if( commonLoader == null ) {
                // no config file, default to this loader - we might be in a 'single' env.
                commonLoader=this.getClass().getClassLoader();
            }
            catalinaLoader = createClassLoader("server", commonLoader);
            sharedLoader = createClassLoader("shared", commonLoader);
        } catch (Throwable t) {
            log.error("Class loader creation threw exception", t);
            System.exit(1);
        }
    }

二是,tomcat 6开始,将原来common、server、shared目录中的类库移动到了lib目录进行合并,统一由common加载器加载,相应的取消了server、shared的加载。具体的实现其实就是将三者在catalina.properties中的配置进行了修改,如下

common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar

server.loader=

shared.loader=

这是默认的行为,用户可以进行配置更改。可以看到tomcat通过将server.loader和shared.loader属性置空,不用修改Bootstrap类中的initClassLoaders方法,就可以完成这个类加载器的合并(也就是说,实际上server和shared的类加载器还是在运行时实例化了的,只是都被设置为了Common加载器的实例)。

二、打破树级结构

对传统类加载器父级结构的“破坏”的一个典型例子是OSGi。

OSGi的各个Bundle通过声明其依赖的package,以及Bundle发布自己的package,使得有依赖的Bundle可以向发布了package的Bundle发起类加载的委派。而这样产生的类加载器的结构变得很复杂,不在是单纯的树结构了,而是一种网状结构。

由于对OSGi没有研究过,这里就不具体的展开说明了。待以后学习OSGi后再补充完善。

推荐阅读更多精彩内容