vlambda博客
学习文章列表

06-阿里面试题:Tomcat类加载设计 [线程上下文类加载器+破坏性双亲委派机制]

点击上方蓝色“OpenCoder”关注,选择“设为星标”

Tomcat这种web容器中的类加载器应该如何设计实现?首先我们来看下Tomcat类加载器的设计结构:

那么应用程序类加载器下的都是Tomcat自定义的类加载器,Tomcat为什么要自定义这么多类加载器又分别有什么用呢?

我们通过以下一张图来进行说明:

首先Tomcat会通过Common类加载器来加载本地lib包下的核心文件,比如servlet-api.jar、jsp-api.jar、el-api.jar等,这些类可以供Tomcat以及所有的WebApp进行访问和使用。

可以通过查看 conf/catalina.properties配置文件查看

common.loader=" {catalina.base}/lib/.jar"," {catalina.home}/lib/.jar"

其次Catalina类加载器加载Tomcat应用程序所独有的一些类文件,这些文件对所有WebApp不可见,比如实现自己的会话存储方案。其路径由server.loader指定,默认为空,可以手动更改指定。

可以通过查看 conf/catalina.properties配置文件查看

server.loader=

再次,Shared类加载器负责加载Web应用共享类,这些类tomcat服务器不会依赖。

可以通过查看 conf/catalina.properties配置文件查看

shared.loader=

而我们的WebApp类加载器主要是加载我们每个应用程序自己编写的代码,主要路径为: /WEB-INF/classes/目录下的Class和资源文件  以及 /WEB-INF/lib目录下的jar包,该类加载器加载的资源仅对当前应用程序可见,其他应用程序无法访问。并且WebApp类加载器可以使用到上级Shared类加载器加载到的类。

最后JSP类加载器是为每一个JSP文件单独设计的一个类加载器,这也能解释为什么JSP文件被修改后不用重启服务器就能实现新的代码功能,这也是现在的热部署方案原因。当某一个JSP文件被修改后,对应的类加载器会被销毁重新创建新的一个JSP类加载器进行加载。

问题思考:

当我们的服务器中有多个应用程序的时候,并且都使用到了Spring来进行组织和管理,那么我们就可以把Spring放到Shared类加载器路径下进行加载,让所有应用程序进行共享,我们自己写的代码由于是WebApp加载器加载的所以访问上级Shared加载器加载的类是没问题的。但是Spring中的类要对应用程序中的类进行管理,如何访问呢?根据我们上文所说的双亲委派机制,显然是无法做到让上级类加载器去请求下级类加载器进行类加载的动作的。因此这里我们需要引出上下文类加载器机制。(如下图)

破坏双亲委派机制

上文提到过双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型,但也有例外的情况,直到Java模块化出现为止,双亲委派模型主要出现过3次较大规模“被破坏”的情况。

双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则,如果有基础类型又要调用回用户的代码,那该怎么办呢?

这并非是不可能出现的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务, 它的代码由启动类加载器来完成加载(在JDK 1.3时加入到rt.jar的),肯定属于Java中很基础的类型了。但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么办?

为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器 (Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

有了线程上下文类加载器,程序就可以做一些“舞弊”的事情了。JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,但也是无可奈何的事情。Java中涉及SPI的加载基本上都采用这种方式来完成,例如JNDI、 JDBC、JCE、JAXB和JBI等。

其实Tomcat中的JSP类加载器的设计就是一种热部署的实现,也是一种打破了双亲委派模型的一种设计。这里“被破坏”并不一定是带有贬义的。只要有明确的目的和充分的理由,突破旧有原则无疑是一种创新。

总结:

按主流的双亲委派机制,显然无法做到让父类加载器加载的类去访问子类加载器加载的类,但使用线程上下文加载器,可以让父类加载器请求子类加载器去完成类加载的动作。spring加载类所用的Classloader是通过Thread.currentThread().getContextClassLoader()来获取的,而当线程创建时会默认setContextClassLoader(AppClassLoader),即线程上下文类加载器被设置为AppClassLoader,spring中始终可以获取到这个AppClassLoader(在Tomcat里就是WebAppClassLoader)子类加载器来加载bean,以后任何一个线程都可以通过getContextClassLoader()获取到WebAppClassLoader来getbean了。

下一篇文章我们将深入Tomcat启动过程的源码分析来验证我们的类加载器机制