暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

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

蹲厕所的熊 2018-07-12
659

蹲厕所的熊 转载请注明原创出处,谢谢!

前言

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

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

  • Thread.getContextClassLoader() 获取到的classLoader和 getClass().getClassLoader() 的有什么区别?

  • 什么场景下会用到 Thread.getContextClassLoader() 的方式获取classLoader?

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

SPI加载问题

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

  1. public static <S> ServiceLoader<S> load(Class<S> service) {

  2.    // 获取当前调用线程的类加载器

  3.    ClassLoader cl = Thread.currentThread().getContextClassLoader();

  4.    return ServiceLoader.load(service, cl);

  5. }

复制

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,具体方法如下(删去了部分不相关内容):

  1. public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {

  2.    try {

  3.        // 创建WebApplicationContext

  4.        if (this.context == null) {

  5.            this.context = createWebApplicationContext(servletContext);

  6.        }

  7.        // 将其保存到该webapp的servletContext中    

  8.        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

  9.        // 获取线程上下文类加载器,默认为WebAppClassLoader

  10.        ClassLoader ccl = Thread.currentThread().getContextClassLoader();

  11.        // 如果spring的jar包放在每个webapp自己的目录中

  12.        // 此时线程上下文类加载器会与本类的类加载器(加载spring的)相同,都是WebAppClassLoader

  13.        if (ccl == ContextLoader.class.getClassLoader()) {

  14.            currentContext = this.context;

  15.        } else if (ccl != null) {

  16.            // 如果不同,也就是上面说的那个问题的情况,那么用一个map把刚才创建的WebApplicationContext及对应的WebAppClassLoader存下来

  17.            // 一个webapp对应一个记录,后续调用时直接根据WebAppClassLoader来取出

  18.            currentContextPerThread.put(ccl, this.context);

  19.        }


  20.        return this.context;

  21.    } catch (RuntimeException ex) {

  22.        logger.error("Context initialization failed", ex);

  23.        throw ex;

  24.    } catch (Error err) {

  25.        logger.error("Context initialization failed", err);

  26.        throw err;

  27.    }

  28. }

复制

总结

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

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



如果读完觉得有收获的话,欢迎点赞、关注、加公众号【蹲厕所的熊】,查阅更多精彩历史!!!

文章转载自蹲厕所的熊,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论