蹲厕所的熊 转载请注明原创出处,谢谢!
前言
相信有一定基础的同学对类加载器(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未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。
如果读完觉得有收获的话,欢迎点赞、关注、加公众号【蹲厕所的熊】,查阅更多精彩历史!!!