蹲厕所的熊

benjaminwhx

理解TCCL:线程上下文类加载器

2018-07-11 作者: 吴海旭


  1. 前言
  2. SPI加载问题
  3. Tomcat与Spring的加载问题
  4. 总结

前言

相信有一定基础的同学对类加载器(ClassLoader)以及类加载机制不会陌生,如果你还不了解什么是类加载器,双亲委派模型是什么的话,先 戳我 去学习~

在看JDK、Tomcat以及Spring源码的时候,会经常的出来 Thread.getContextClassLoader() 这句代码,一开始我也似懂非懂的理解,但是还是不知道它的使用场景是什么,总结起来有以下几个问题:

  • Thread.getContextClassLoader() 获取到的classLoader和 getClass().getClassLoader() 的有什么区别?
  • 什么场景下会用到 Thread.getContextClassLoader() 的方式获取classLoader?

下面我们会从JDK的SPI机制、Tomcat以及Spring三个方面去剖析。

SPI加载问题

之前我写过一篇 SPI机制 的文章,其中ServiceLoader的load方法是获取到了TCCL来加载用户的类的,那么有同学想过是为什么吗?

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取当前调用线程的类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

Java提供了很多SPI,允许第三方为这些接口提供实现,最常见的SPI实现有JDBC、JNDI等等,根据类加载器的双亲委派模型,加载ServiceLoader的 BootstrapClassLoader 是不能加载SPI的实现类的,因为SPI的实现类是由 AppClassLoader 加载的,而 BootstrapClassLoader 是不能委派 AppClassLoader 来加载类的,那该怎么办呢?

线程上下文类加载器正好解决了这个问题,默认情况下,Java应用的线程上下文类加载器默认是AppClassLoader,这样ServiceLoader就可以成功加载SPI的实现类了。

Tomcat与Spring的加载问题

Tomcat基本遵守了JVM的委派模型,但也在自定义的类加载器中做了细微的调整,以适应Tomcat自身的要求。下面是一张Tomcat的类加载体系图:

  • CommonClassLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问。
  • CatalinaClassLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见。
  • SharedClassLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见。
  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见。

稍微做一下小结:CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。

这时有的同学要问了,如果有10个web应用程序都用到了Spring的话,可以把Spring的jar包放到common或者shared目录下让这些程序共享。Spring 的作用是管理每个web应用程序的bean,getBean时自然要能访问到应用程序的类,而用户的程序显然是放在 /WebApp/WEB-INF 目录中的(由 WebAppClassLoader 加载),那么在 CommonClassLoader 或 SharedClassLoader 中的 Spring 容器如何去加载并不在其加载范围的用户程序(/WebApp/WEB-INF/)中的Class呢?

原来spring根本不会去管自己被放在哪里,它统统使用TCCL来加载类,而TCCL默认设置为了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;
    }
}

总结

通过前面结合SPI、Tomcat以及Spring中TCCL的应用场景,相信前面提的那两个问题就都容易解决了。最后,我们总结一下线程上下文类加载器的适用场景:

  1. 当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
  2. 当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。


坚持原创技术分享,您的支持将鼓励我继续创作!



分享

评论