在分析 tomcat 类加载之前,我们简单的回顾下 java 体系的类加载器
ClassLoader
,而是 jvm 层面由 C/C++ 实现的类加载器,负责加载 $JAVA_HOME/jre/lib 目录下 jvm 指定的类库,它是无法被 java 应用程序直接使用的ClassLoader.getSystemClassLoader()
获取,它也是由启动类加载器加载的ClassLoader
,当然也可以不继承下图描述了类加载器的关系图,其中自定义类加载器有N多个
我们知道 java.lang.ClassLoader
有双亲委派机制(准确的说是单亲,因为只有一个parent),这只是 java 建议的规范,我们也可以不遵循这条规则,但是建议遵循该规则。此外,有一点需要注意的是,类加载器不局限于 ClassLoader
,我们也可以自己实现一个类加载器,只要你加载出来的 Class 符合 jvm 规范即可
我们在日常开发工作中,经常会遇到类冲突的情况,明明 classpath 下面的类有这个方法,但是一旦跑线上环境就出错,比如NoSuchMethodError
、NoClassDefFoundError
、NoClassDefFoundError
等。我们可以使用 jvm 参数 -verbose:class
方便地定位该问题,使用该参数可以快速地定位某个类是从哪个jar包加载的,而不是一味地埋头苦干,求百度,找Google。下面是使用 -verbose:class
jvm 参数的部分日志输出
[Loaded org.springframework.context.annotation.CommonAnnotationBeanPostProcessor from file:/D:/tomcat/webapps/touch/WEB-INF/lib/spring-context-4.3.7.RELEASE.jar]
[Loaded com.alibaba.dubbo.rpc.InvokerListener from file:/D:/tomcat/webapps/touch/WEB-INF/lib/dubbo-2.5.3.jar]
我们有必要了解下关于类加载有几个重要的知识点:
CloassLoader
,并且会从父线程中继承(默认是应用类加载器),在没有显式声明由哪个类加载器加载类时(比如 new 关键字),将默认由当前线程的类加载器加载该类由于篇幅有限,关于类加载的过程这里不再展开了,可以参考厮大的博客
根据实际的应用场景,我们来分析下 tomcat 类加载器需要解决的几个问题
为了解决以上问题,tomcat设计了一套类加载器,如下图所示。在 Tomcat 里面最重要的是 Common 类加载器,它的父加载器是应用程序类加载器,负责加载 ${catalina.base}/lib
、${catalina.home}/lib
目录下面所有的 .jar 文件和 .class 文件。下图的虚线部分,有 catalina 类加载器、share 类加载器,并且它们的 parent 是 common 类加载器,默认情况下被赋值为 Common 类加载器实例,即 Common 类加载器、catalina 类加载器、 share 类加载器都属于同一个实例。当然,如果我们通过修改 catalina.properties
文件的 server.loader
和 shared.loader
配置,从而指定其创建不同的类加载器
我们先从 Bootstrap
这个入口说起,在执行 init
的时候会实例化类加载器,在初始化类加载器之后立即设置线程上下文类加载器(Thread Context ClassLoader)为 catalina 类加载器,接下来是为 Catalina
组件指定父类加载器。为什么要设置线程上下文的类加载器呢?一方面,很多诸如 ClassUtils
之类的编码,他们在获取 ClassLoader
的时候,都是先尝试从 Thread 上下文中获取 ClassLoader
,例如:ClassLoader cl = Thread.currentThread().getContextClassLoader();
另一方面,在没有显式指定类加载器的情况下,默认使用线程的上下文类加载器加载类,由于 tomcat 的大部分 jar 包都在 ${catalina.hom}/lib
目录,因此需要将线程类加载器指定为 catalina 类加载器,否则加载不了相关的类。
双亲委派模型存在设计上的缺陷,在某些应用场景下,例如加载 SPI 实现(JNDI、JDBC等),如果我们严格遵循双亲委派的一般性原则,使用应用程序类加载器,由于这些 SPI 实现在厂商的 jar 包中,所以应用程序类加载器不可能认识这些代码啊,怎么办?为了解决这个问题,Java 设计团队引入了一个不太优雅的设计:Thread Context ClassLoader
,有了这个线程上下文类加载器,我们便可以做一些“舞弊”的事情了,JNDI 服务可以使用这个类加载器加载 SPI 需要的代码,JDBC、JAXB 也是如此。这样,双亲委派模型便被破坏了。
Bootstrap.java
public void init() throws Exception {// 初始化commonLoader、catalinaLoader、sharedLoader,关于ClassLoader的后面再看initClassLoaders();// 设置上下文类加载器为 catalinaLoader Thread.currentThread().setContextClassLoader(catalinaLoader);SecurityClassLoad.securityClassLoad(catalinaLoader);// 反射方法实例化Catalina,后面初始化Catalina用了很多反射,不知道意图是什么Class> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");Object startupInstance = startupClass.getConstructor().newInstance();//TODO 为Catalina对象设置其父加载器为shared类加载器,默认情况下就是catalina类加载器// 引用Catalina实例catalinaDaemon = startupInstance;
catalina.properties 文件的相关配置如下所示
common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar
server.loader=
我们再来看下创建类加载器的代码,首先是创建 common 类加载器,从 catalina.properties
中读取 common.loader
配置作为 common 类加载器的路径。我们注意到 common.loader
中存在 ${catalina.base}
、${catalina.home}
这样的占位符,在读取配置之后,tomcat 会进行替换处理,同理 server.loader
、shared.loader
也可以使用这样的占位符,或者系统变量作为占位符,有兴趣的童鞋可以参考下 Bootstrap.replace(String str)
源码,如果在项目中有相同的场景的话,可以直接 copy 该代码。