正统类加载器Tomcat(tomcat二探)

主流的Java Web服务器,如Tomcat、Jetty、WebLogic、WebSphere或其他笔者没有列举的服务器, 都实现了自己定义的类加载器,而且一般还都不止一个。因为一个功能健全的Web服务器,都要解决 如下的这些问题:

部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。(WebApp类加载器)

部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享。(Shared类加载器)

服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响。目前,有许多主流的Java Web服务器自身也是使用Java语言来实现的。因此服务器本身也有类库依赖的问题,一般来说,基于安全考虑,服务器所使用的类库应该与应用程序的类库互相独立。 (Catalina类加载器和Shared类加载器平行)

热部署问题

由于存在上述问题,在部署Web应用时,单独的一个ClassPath就不能满足需求了,所以各种Web服务器都不约而同地提供了好几个有着不同含义的ClassPath路径供用户存放第三方类库,这些路径一般会以“lib”或“classes”命名。被放置到不同路径中的类库,具备不同的访问范围和服务对象,通常每一个目录都会有一个相应的自定义类加载器去加载放置在里面的Java类库。现在笔者就以Tomcat服务 器[1]为例,与读者一同分析Tomcat具体是如何规划用户类库结构和类加载器的。

在Tomcat目录结构中,可以设置3组目录(/common/*、/server/和/shared/,但默认不一定是开放 的,可能只有/lib/目录存在)用于存放Java类库,另外还应该加上Web应用程序自身的“/WEBINF/”目录,一共4组。把Java类库放置在这4组目录中,每一组都有独立的含义,分别是: ·放置在/common目录中。类库可被Tomcat和所有的Web应用程序共同使用。 ·放置在/server目录中。类库可被Tomcat使用,对所有的Web应用程序都不可见。 ·放置在/shared目录中。类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。 ·放置在/WebApp/WEB-INF目录中。类库仅仅可以被该Web应用程序使用,对Tomcat和其他Web应 用程序都不可见。 为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器, 这些类加载器按照经典的双亲委派模型来实现,其关系如图9-1所示。
在这里插入图片描述

灰色背景的3个类加载器是JDK(以JDK 9之前经典的三层类加载器为例)默认提供的类加载器, 这3个加载器的作用在之前类加载器的文章中已经介绍过了。而Common类加载器、Catalina类加载器(也称为Server类 加载器)、Shared类加载器和Webapp类加载器则是Tomcat自己定义的类加载器,它们分别加 载/common/、/server/、/shared/*和/WebApp/WEB-INF/*中的Java类库。

其中WebApp类加载器和JSP类加载器通常还会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应 一个JasperLoader类加载器。 从图9-1的委派关系中可以看出,Common类加载器能加载的类都可以被Catalina类加载器和Shared 类加载器使用,而Catalina类加载器和Shared类加载器自己能加载的类则与对方相互隔离。WebApp类加载器可以使用Shared类加载器加载到的类,但各个WebApp类加载器实例之间相互隔离。而 JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个Class文件,它存在的目的就是为了被丢弃:当服务器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新 的JSP类加载器来实现JSP文件的HotSwap功能。 本例中的类加载结构在Tomcat6以前是它默认的类加载器结构,在Tomcat6及之后的版本简化了默 认的目录结构,只有指定了tomcat/conf/catalina.properties配置文件的server.loader和share.loader项后才会 真正建立Catalina类加载器和Shared类加载器的实例,否则会用到这两个类加载器的地方都会用 Common类加载器的实例代替,而默认的配置文件中并没有设置这两个loader项,所以Tomcat 6之后也顺理成章地把/common、/server和/shared这3个目录默认合并到一起变成1个/lib目录,这个目录里的类库 相当于以前/common目录中类库的作用,是Tomcat的开发团队为了简化大多数的部署场景所做的一项 易用性改进。如果默认设置不能满足需要,用户可以通过修改配置文件指定server.loader和share.loader 的方式重新启用原来完整的加载器架构Tomcat加载器的实现清晰易懂,并且采用了官方推荐的“正统”的使用类加载器的方式。如果读者阅读完上面的案例后,毫不费力就能完全理解Tomcat设计团队这样布置加载器架构的用意,这就说明你已经大致掌握了类加载器“主流”的使用方式,

那么笔者不妨再提一个问题让各位读者思考一下:前面曾经提到过一个场景,如果有10个Web应用程序都是用Spring来进行组织和管理的话,可以把Spring 放到Common或Shared目录下让这些程序共享。Spring要对用户程序的类进行管理,自然要能访问到用户程序的类,而用户的程序显然是放在/WebApp/WEB-INF目录中的。那么被Common类加载器或 Shared类加载器加载的Spring如何访问并不在其加载范围内的用户程序呢?如果你读懂了本书第7章的相关内容,相信回答这个问题一定会毫不费力。

答案:

spring根本不会去管自己被放在哪里,它统统使用线程上下文加载器来加载类,而线程上下文加载器默认设置为了WebAppClassLoader,也就是说哪个WebApp应用调用了spring,spring就去取该应用自己的WebAppClassLoader来加载bean,

源码分析

有兴趣的可以接着看看具体实现。在web.xml中定义的listener为org.springframework.web.context.ContextLoaderListener

它最终调用了org.springframework.web.context.ContextLoader类来装载bean,具体方法如下(删去了部分不相关内容):

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    try {
        // 创建WebApplicationContext
		if (this.context == null) {
            this.context = createWebApplicationContext(servletContext);
        }
        // 将其保存到该webapp的servletContext中  
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
        // 获取线程上下文类加载器,默认为WebAppClassLoader
        ClassLoader ccl = Thread.currentThread().getContextClassLoader();
        // 如果spring的jar包放在每个webapp自己的目录中
        // 此时线程上下文类加载器会与本类的类加载器(加载spring的)相同,都是WebAppClassLoader
        if (ccl == ContextLoader.class.getClassLoader()) {
            currentContext = this.context;
        } else if (ccl != null) {
            // 如果不同,也就是上面说的那个问题的情况,那么用一个map把刚才创建的WebApplicationContext           
            //及对应的WebAppClassLoader存下来 一个webapp对应一个记录,
            //后续调用时直接根据WebAppClassLoader来取出
            currentContextPerThread.put(ccl, this.context);
        }
        return this.context;
    } catch (RuntimeException ex) {
        logger.error("Context initialization failed", ex);
        throw ex;
    } catch (Error err) {
        logger.error("Context initialization failed", err);
        throw err;
    }
    
}

具体说明都在注释中,spring考虑到了自己可能被放到其他位置,所以直接用线程上下文类加载器来解决所有可能面临的情况。

总结

通过上面的两个案例分析,我们可以总结出线程上下文类加载器的适用场景:

  1. 当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,

必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。

  1. 当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,

可以取调用者各自的线程上下文类加载器代为托管。

简而言之就是ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,

所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值