类加载器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后再补充完善。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容