欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈
1. Tomcat启动
上篇文章中,我们已经讲到了Tomcat的启动入口Bootstrap、Shell程序Catalina以及各个组件之间的关系
,尤其是统一的生命周期管理接口Lifecycle。在应用服务器启动过程中,我们会充分体会到它的便利性。基于上面的静态设计,简化的启动过程如图所示:
从图中我们可以看出,Tomcat的启动过程非常标准化,统一按照生命周期管理接口Lifecycle 的定义进行启动。首先,调用init()方法进行组件的逐级初始化
,然后再调用start()方法进行 启动
。当然,每次调用均伴随着生命周期状态变更事件的触发
。
每一级组件除完成自身的处理外,还要负责调用子组件相应的生命周期管理方法,组件与组 件之间是松耦合的设计,因此我们很容易通过配置进行修改和替换。下面跟进源码分析启动过程。
1.1 启动脚本 startup.bat或startup.sh
- 在使用startiup.bat文件的时候最终还是调用的是catalina.bat文件
setlocal
rem Guess CATALINA_HOME if not defined
set "CURRENT_DIR=%cd%" //将当前的路径赋值给CURRENT_DIR
if not "%CATALINA_HOME%" == "" goto gotHome //如果在系统环境变量中有配置CATALINA_HOME则直接跳转到gotHome
set "CATALINA_HOME=%CURRENT_DIR%" //如果没配置则将当前的路径赋值给CATALINA_HOME
if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome //如果当前赋值的下由catalina.bat文件的存在则跳转到okHome
cd ..
set "CATALINA_HOME=%cd%"
cd "%CURRENT_DIR%"
:gotHome
if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome
echo The CATALINA_HOME environment variable is not defined correctly
echo This environment variable is needed to run this program
goto end
:okHome
set "EXECUTABLE=%CATALINA_HOME%\bin\catalina.bat" //将catalina.bat给变量EXECUTABLE
rem Check that target executable exists
if exist "%EXECUTABLE%" goto okExec //EXECUTABLE如果存在直接跳转okExec
echo Cannot find "%EXECUTABLE%" //打印没找到EXECUTABLE的存在
echo This file is needed to run this program
goto end
:okExec
rem Get remaining unshifted command line arguments and save them in the
set CMD_LINE_ARGS=
:setArgs
if ""%1""=="""" goto doneSetArgs //判断有没有参数
set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1 //设置第一个参数
shift
goto setArgs
:doneSetArgs
call "%EXECUTABLE%" start %CMD_LINE_ARGS% //转发调用catalina.bat文件启动 参数为CMD_LINE_ARGS
:end
复制代码
1.2 catalian.bat文件
catalina.bat是tomcat启动的主要程序
,不管使用startup.bat还是启动还是使用shuoutdown.bat进行关闭的时候都用到了catalina.bat文件- catalina.bat文件可以进行参数的传递,实际上就是
java命令调用Bootstrap类
,catalina编写是windows编程和shell编程类似,都是用来操作一些命令的语言。 - catalina.bat文件内有三句重要的代码
- set "CLASSPATH=%CLASSPATH%%CATALINA_HOME%\bin\bootstrap.jar":加载bin目录下的bootstrap.jar文件
- set MAINCLASS=org.apache.catalina.startup.Bootstrap:获取到启动类
- set ACTION=start:启动启动类的main方法
1.3 Bootstrap类的启动流程
Bootstrap类的main方法为tomcat启动的入口
,如下:
//bootstrap的main方法
public static void main(String args[]) {
//注意启动的时候是同步的
synchronized (daemonLock) {
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init();//调用初始化方法,跟进去看看
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
...
复制代码
init方法
主要是完成加载器的配置和初始化的准备
,Bootstrap内的调用的init方法主要就是为了做启动前的各个变量的值的赋予。
public void init() throws Exception {
//初始化类三个加载器,后面详细介绍
initClassLoaders();
//设置线程类加载器,将容器的加载器传入
Thread.currentThread().setContextClassLoader(catalinaLoader);
//加载安全类加载
SecurityClassLoad.securityClassLoad(catalinaLoader);
//下面是初始化日志
// Load our startup class and call its process() method
if (log.isDebugEnabled())
log.debug("Loading startup class");
//通过反射加载catalina类
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
//创建对象
Object startupInstance = startupClass.getConstructor().newInstance();
// Set the shared extensions class loader
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
//利用反射创建类加载器的对象调用方法为setParentClassLoader方法
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");//将类加载器作为参数进行传递
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;//共享加载器
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);//返回方法对类加载器进行初始值赋值
method.invoke(startupInstance, paramValues);//调用了catalina内的setParentClassLoader方法对catalina类内的类加载器赋值
catalinaDaemon = startupInstance;//将创建好的startupInstance 对象赋值给catalinaDaemon (此时的startupInstance 指的是Catalina对象用于load中调用Catalina类的load方法)
}
复制代码
执行完毕init()方法将执行的load方法
- 执行到这里init()方法就结束了接下来要执行的load方法了此代码是bootstrap内main方法内的代码它有个初始参数command为start
try {
//执行参数初始为启动 发现可以使用两个参数start和startd进行启动
String command = "start";
if (args.length > 0) {
command = args[args.length - 1];
}
if (command.equals("startd")) {
args[args.length - 1] = "start";
//在这里启动的链式初始化
daemon.load(args);
daemon.start();
} else if (command.equals("stopd")) {
args[args.length - 1] = "stop";
daemon.stop();
} else if (command.equals("start")) {
daemon.setAwait(true);
// 服务器大组件初始化流程,在这里绑定了ServerSocket端口,准备接收数据了
daemon.load(args);
// 启动
daemon.start();
if (null == daemon.getServer()) {
System.exit(1);
}
}
...
} catch (Throwable t) {
...
}
复制代码
load是通过反射调用了Catalina类中的load方法
- 在init方法中通过反射创建的Catalina的对象赋值给了
catalinaDaemon
变量。 - 在load方法内直接使用反射调用了Catalina类的load方法
private void load(String[] arguments) throws Exception {
// Call the load() method
//设置调用的方法名称
String methodName = "load";
//设置传递的参数
Object param[];
Class<?> paramTypes[];
if (arguments==null || arguments.length==0) {
paramTypes = null;
param = null;
} else {
paramTypes = new Class[1];
paramTypes[0] = arguments.getClass();
param = new Object[1];
param[0] = arguments;
}
//开始远程调用Catalina类中的方法
Method method =
catalinaDaemon.getClass().getMethod(methodName, paramTypes);
if (log.isDebugEnabled()) {
log.debug("Calling startup class " + method);
}
//Catalina load方法是没参数的看源码就是知道param指向的main的args参数也即是null
method.invoke(catalinaDaemon, param);
}
复制代码
1.4 Catalina类中load方法
- load方法是有重载的方法的不过最终调用的还是无参的
- Catalina的load方法主要做的是创建一个Server组件的实例
// catalina的加载信息
public void load() {
if (loaded) {
return;
}
loaded = true;
long t1 = System.nanoTime();
initDirs();
// Before digester - it may be needed
initNaming();
// Parse main server.xml
// 解析服务器的server.xml文件 使用Digester 技术进行xml文档对象解析
parseServerXml(true);
Server s = getServer();
if (s == null) {
return;
}
getServer().setCatalina(this);
getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
// Stream redirection
initStreams();
// Start the new server
try {
// 服务器执行初始化 开始调用的Server的初始化方法注意Server是一个接口
getServer().init();
} catch (LifecycleException e) {
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
throw new java.lang.Error(e);
} else {
log.error(sm.getString("catalina.initError"), e);
}
}
if(log.isInfoEnabled()) {
log.info(sm.getString("catalina.init", Long.toString(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - t1))));
}
}
public void load(String args[]) {
try {
if (arguments(args)) {
load();
}
} catch (Exception e) {
e.printStackTrace(System.out);
}
}
复制代码
其实到此tomcat启动的就讲通了,根据上图的启动结构顺着找就行了
- 在Bootstarp内进行初始化全部组件调用catalina类中的load方法加载其他组件,对其他的组件进行初始化
- 初始化全部组件后在进行启动整个tomcat
- 在bootstrap类中调用的load方法会调用到catalina类中的load方法加载servler.xml文件内内容。
- load方法内会完成对server.xml文件的解析并将解完成的数据封装成StandardServer对象。
1.5 启动过程说明
1.5.1 Lifecycle 接口
Lifecycle
提供一种统一的管理对象生命周期的接口
。通过 Lifecycle、LifecycleListener、 LifecycleEvent
,Catalina 实现了对 tomcat 各种组件、容器统一的启动和停止的方式。 在 Tomcat 服 务 开 启 过 程 中 启 动 的 一 些 列 组 件 、 容 器 , 都 实 现 了 org.apache.catalina.Lifecycle 这个接口,其中的 init()、start() 方法、stop() 方法,为其子类实 现了统一的 start 和 stop 管理。
1.5.2 load 方法解析 server.xml 配置文件
load 方法解析 server.xml
配置文件,并加载 Server、Service、Connector、Container、Engine、 Host、Context、Wrapper 一系列的容器。加载完成后,调用 initialize()来开启一个新的 Server
1.5.3 Digester 类解析 server.xml 文件
利用 Digester 类解析 server.xml 文件,得到容器的配置。这个我们后面在讲
1.5.4 demon.start()
demon.start()
方法会调用 Catalina 的 start 方法 Catalina 实例执行 start 方法。这里有两个点,一个是 load()加载 server.xml 配置、初始 化 Server 的过程,一个是 getServer().start()开启服务、初始化并开启一系列组件、子容器的 过程。
public void start() throws Exception {
if (catalinaDaemon == null) {
init();
}
// 反射调用 satrt()方法
Method method = catalinaDaemon.getClass().getMethod("start", (Class [])null);
method.invoke(catalinaDaemon, (Object [])null);
}
复制代码
1.5.5 StandardServer
service.initialize()
然后拿到 StandardServer 实例调用 initialize()方法初始化。 Tomcat 容器的一系列组件。一些容器初始化的的时候,都会调用其子容器的 initialize()方法,初始化它的子容器。顺序是 StandardServer、StandardService、StandardEngine、Connector。每个容器都在初始化自身相 关设置的同时,将子容器初始化。
tomcat启动的时候我们发现是一个的链条式的启动方式
,每初始化完成一个组件,会在组件内调用用下一个的组件的init方法加载下一个组件的初始化方法。同样在启动的时候也是同样,在bootstrap类中继续执行start方法完成加载的操作。
1.6 总结
tomcat的启动--bootstrap类为入口,以Catalian类为起点,以初始化为优先依次初始化tomcat所有组件,然后在依次启动结构还是很清晰的。
2. 类加载器
下载tomcat解压后,可以在webapps目录下看到几个文件夹(这些都是web应用),webapps对应到tomcat容器中的Host,里面的文件夹则对应到Context
。tomcat启动后,webapps下的所有web应用都可以提供服务。
那么就有一个问题,假如webapps下有两个应用app1和app2,它们有各自独立依赖的jar包,又有共同依赖的jar包,这些相同的jar包有些版本相同,有些又不相同,这种情况下,tomcat是如何加载这些jar包的呢?
带着这个疑问,一步步来分析tomcat的类加载机制吧。
2.1 Java类加载机制
在这之前,当然要先了解一下java中类加载时怎样的,毕竟tomcat是用java写的,它的加载机制也是基于java的类加载机制。
2.1.1 类加载器
1.什么是类加载器?
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。
2.如何判断两个类是否相等?
类加载器用于实现类的加载动作。对于任意一个类,都需要由加载它的类加载器和这个类本身共同确立其在Java虚拟机中的唯一性,每一个类,都拥有一个独立的类名称空间。也就是说:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
2.1.2 双亲委派模型
Java 提供三种类型的系统类加载器:
启动类加载器(Bootstrap ClassLoader)
:由C++语言实现,属于JVM的一部分,其作用是加载 <JAVA_HOME>\lib 目录中的文件,或者被-Xbootclasspath参数所指定的路径中的文件,并且该类加载器只加载特定名称的文件(如 rt.jar),而不是该目录下所有的文件。启动类加载器无法被Java程序直接引用。扩展类加载器(Extension ClassLoader)
:由sun.misc.Launcher.ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。应用程序类加载器(Application ClassLoader)
:也称系统类加载器,由sun.misc.Launcher.AppClassLoader实现。负责加载用户类路径(Class Path)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
应用程序都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。加载流程如下图所示:
用一段代码测试一下:
public static void main(String[] args) {
ClassLoader loader = Xxx.class.getClassLoader();
while (loader!=null){
System.out.println(loader);
loader = loader.getParent();
}
}
复制代码
结果:
从结果我们可以看出,默认情况下,用户自定义的类使用 AppClassLoader 加载,AppClassLoader 的父加载器为 ExtClassLoader,但是 ExtClassLoader 的父加载器却显示为空,这是什么原因呢?究其缘由,启动类加载器属于 JVM 的一部分,它不是由 Java 语言实现的,在 Java 中无法直接引用,所以才返回空。
java这种类加载层级称为**双亲委派模型**
。它的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
为什么要这样呢?
都知道java.lang.Object是java中所有类的父类
,它存放在rt.jar
之中,按照双亲委派模型
,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类
。试想,如果没有使用双亲委派模型,由各个类加载器自行去加载,显然,这就存在很大风险,用户完全可以恶意编写一个java.lang.Object类,然后放到ClassPath下,那系统就会出现多个Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。
2.1.3、简单看下源码
protected Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
synchronized(this.getClassLoadingLock(var1)) {
//首先, 检查请求的类是否已经被加载过了
Class var4 = this.findLoadedClass(var1);
if(var4 == null) {
long var5 = System.nanoTime();
try {
if(this.parent != null) {
var4 = this.parent.loadClass(var1, false);
} else {
var4 = this.findBootstrapClassOrNull(var1);
}
} catch (ClassNotFoundException var10) {
//如果父类加载器抛出ClassNotFoundException
//说明父类加载器无法完成加载请求
;
}
if(var4 == null) {
long var7 = System.nanoTime();
//在父类加载器无法加载的时候
//再调用本身的findClass方法来进行类加载
var4 = this.findClass(var1);
PerfCounter.getParentDelegationTime().addTime(var7 - var5);
PerfCounter.getFindClassTime().addElapsedTimeFrom(var7);
PerfCounter.getFindClasses().increment();
}
}
if(var2) {
this.resolveClass(var4);
}
return var4;
}
}
复制代码
从源码可以看出,ExtClassLoader
和 AppClassLoader
都继承自 ClassLoader 类,ClassLoader 类中通过 loadClass 方法来实现双亲委派机制
。整个类的加载过程可分为如下三步:
- 查找对应的类是否已经加载。
- 若未加载,则判断当前类加载器的父加载器是否为空,不为空则委托给父类去加载,否则调用启动类加载器加载(findBootstrapClassOrNull 再往下会调用一个 native 方法)。
- 若第二步加载失败,说明父类加载器无法完成加载请求 ,则调用当前类加载器加载。
详细可以参考这篇博文:
2.1.4 打破双亲委派模型(来自《深入理解Java虚拟机:JVM高级特性与最佳实践》)
双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。
它很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API,但世事往往没有绝对的完美,如果基础类又要调用回用户的代码,那该怎么办?
这并非是不可能的事情,一个典型的例子便是JNDI服务,它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”之些代码,该怎么办?
Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器(Application ClassLoader)。
有了线程上下文类加载器,就可以做一些“舞弊”的事情了,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则。
还有“被破坏”是由于用户对程序的动态性的追求导致的,例如OSGi的出现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。
当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:
1)将以java.*开头的类委派给父类加载器加载。
2)否则,将委派列表名单内的类委派给父类加载器加载。
3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
4)否则,查找当前Bundle的Class Path,使用自己的类加载器加载。
5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7)否则,类查找失败。
2.2 Tomcat类加载器
了解了java的双亲委派模型,现在回到正题上,tomcat的类加载器是怎么样的?
2.2.1、Web容器应该具备的特性
不难想象,一个功能健全的Web容器,它的类加载器必然有多个,因为它应该具备如下特性:
**隔离性**
:部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以实现相互隔离。设想一下,两个Web应用,一个使用了Spring2.5,另一个使用了教新的4.0,应用服务器使用一个类加载器,Web应用将会因为jar包覆盖而无法启动。**灵活性**
:Web应用之间的类加载器相互独立,那么就能针对一个Web应用进行重新部署,此时Web应用的类加载器会被重建,而且不会影响其他的Web应用。如果采用一个类加载器,类之间的依赖是杂乱复杂的,无法完全移出某个应用的类。**性能**
:部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以互相共享。这个需求也很常见,例如,用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,如果把10份Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费——这主要倒不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到Web容器的内存,如果类库不能共享,虚拟机的方法区就会很容易出现过度膨胀的风险。- ...
2.2.2、tomcat类加载器结构
了解了一款Web容器应该具备的特性,明白了Web容器的类加载器有多个,再来看tomcat的类加载器结构。
首先上张图,整体看下tomcat的类加载器:
可以看到在原先的java类加载器基础上,tomcat新增了几个类加载器,包括3个基础类加载器和每个Web应用的类加载器,其中3个基础类加载器可在conf/catalina.properties中配置,具体介绍下:
Common
:以应用类加载器为父类,是tomcat顶层的公用类加载器,其路径由conf/catalina.properties中的common.loader指定,默认指向${catalina.home}/lib下的包。Catalina
:以Common类加载器为父类,是用于加载Tomcat应用服务器的类加载器,其路径由server.loader指定,默认为空,此时tomcat使用Common类加载器加载应用服务器。Shared
:以Common类加载器为父类,是所有Web应用的父类加载器,其路径由shared.loader指定,默认为空,此时tomcat使用Common类加载器作为Web应用的父加载器。Web应用
:以Shared类加载器为父类,加载/WEB-INF/classes目录下的未压缩的Class和资源文件以及/WEB-INF/lib目录下的jar包,该类加载器只对当前Web应用可见,对其他Web应用均不可见。
默认情况下,Common、Catalina、Shared类加载器是同一个,但可以配置3个不同的类加载器,使他们各司其职。
首先,Common类加载器负责加载Tomcat应用服务器内部和Web应用均可见的类
,如Servlet规范相关包和一些通用工具包。
其次,Catalina类加载器负责只有Tomcat应用服务器内部可见的类,这些类对Web应用不可见
。比如,想实现自己的会话存储方案,而且该方案依赖了一些第三方包,当然是不希望这些包对Web应用可见,这时可以配置server.load,创建独立的Catalina类加载器。
再次,Shared类复杂加载Web应用共享类,这些类tomcat服务器不会依赖
。
相信看到这,引言中的疑问已经解开了吧。
那还有一个问题,如果有10个Web应用程序都是用Spring来进行组织和管理的话,可以把Spring放到Common或Shared目录下让这些程序共享。Spring要对用户程序的类进行管理,自然要能访问到用户程序的类,而用户的程序显然是放在/WebApp/WEB-INF目录中的,那么被CommonClassLoader或SharedClassLoader加载的Spring如何访问并不在其加载范围内的用户程序呢?
如果按主流的双亲委派机制,显然无法做到让父类加载器加载的类去访问子类加载器加载的类,但使用线程上下文加载器,可以让父类加载器请求子类加载器去完成类加载的动作。spring加载类所用的Classloader是通过Thread.currentThread().getContextClassLoader()来获取的,而当线程创建时会默认setContextClassLoader(AppClassLoader),即线程上下文类加载器被设置为AppClassLoader,spring中始终可以获取到这个AppClassLoader(在Tomcat里就是WebAppClassLoader)子类加载器来加载bean,以后任何一个线程都可以通过getContextClassLoader()获取到WebAppClassLoader来getbean了。
接下来,看一看源码。
3. tomcat类加载器源码分析
3.1 Common/Catalina/Shared ClassLoader的创建
首先先看下tomcat的类加载器继承结构:
相信看到这会很疑惑,这和上面介绍的tomcat类加载器结构不一样啊。
是这样的,双亲委派模型本不是通过继承实现的,而是组合
,所以AppClassLoader没有继承自ExtClassLoader,WebappClassLoader也没有继承自AppClassLoader。至于Common ClassLoader,Shared ClassLoader,Catalina ClassLoader则是在启动时初始化的三个不同名字的URLClassLoader。
来到BootStrap中看一下:
public static void main(String args[]) {
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
// When running as a service the call to stop will be on a new
// thread so make sure the correct class loader is used to prevent
// a range of class not found exceptions.
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
...
}
复制代码
先判断Bootstrap是否为null,不为null,直接将Catalina ClassLoader设置到当前线程,用于加载服务器相关类,为null则进入bootstrap的init方法。
public void init() throws Exception {
initClassLoaders();
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
...
}
复制代码
init方法会调用initClassLoaders,同样也会将Catalina ClassLoader设置到当前线程设置到当前线程,进入initClassLoaders来看看。
private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader=this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
复制代码
应该就很明白了,会创建三个ClassLoader,CommClassLoader,Catalina ClassLoader,SharedClassLoader,正好对应前面介绍的三个基础类加载器。
再进入createClassLoader可以看到这三个基础类加载器所加载的资源刚好对应conf/catalina.properties
中的common.loader,server.loader,shared.loader:
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals("")))
return parent;
value = replace(value);
List<Repository> repositories = new ArrayList<>();
String[] repositoryPaths = getPaths(value);
for (String repository : repositoryPaths) {
// Check for a JAR URL repository
try {
@SuppressWarnings("unused")
URL url = new URL(repository);
repositories.add(
new Repository(repository, RepositoryType.URL));
continue;
} catch (MalformedURLException e) {
// Ignore
}
// Local repository
if (repository.endsWith("*.jar")) {
repository = repository.substring
(0, repository.length() - "*.jar".length());
repositories.add(
new Repository(repository, RepositoryType.GLOB));
} else if (repository.endsWith(".jar")) {
repositories.add(
new Repository(repository, RepositoryType.JAR));
} else {
repositories.add(
new Repository(repository, RepositoryType.DIR));
}
}
return ClassLoaderFactory.createClassLoader(repositories, parent);
}
复制代码
3.2 Common/Catalina/Shared ClassLoader的层次构建
Common/Catalina/Shared ClassLoader的创建好了,肯定是要被使用的,是在哪里使用的呢?它们之间同Webapp ClassLoader又是怎么联系起来的?
既然sharedClassLoader被传入到Catalina中,就来看它的getParentClassLoader调用栈。
经过层层调用,来带StandardContext的startInternal方法,这个方法很长很复杂,就不全贴出来,里面有这样一段:
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
复制代码
它会创建WebappLoader对象,并通过setLoader(webappLoader)赋值到一个实例变量中,然后会调用WebappLoader的start方法:
...
if (ok) {
// Start our subordinate components, if any
Loader loader = getLoader();
if (loader instanceof Lifecycle) {
((Lifecycle) loader).start();
}
...
}
...
复制代码
这里关系到tomcat的生命周期机制,先不纠结,直接找到start方法,start方法是在父类中,最后要调回到WebappLoader中的startInternal方法。
该方法中有这样一段:
...
classLoader = createClassLoader();
classLoader.setResources(context.getResources());
classLoader.setDelegate(this.delegate);
...
复制代码
进入createClassLoader方法:
private WebappClassLoaderBase createClassLoader()
throws Exception {
// private String loaderClass = ParallelWebappClassLoader.class.getName();
Class<?> clazz = Class.forName(loaderClass);
WebappClassLoaderBase classLoader = null;
if (parentClassLoader == null) {
parentClassLoader = context.getParentClassLoader();
}
Class<?>[] argTypes = { ClassLoader.class };
Object[] args = { parentClassLoader };
Constructor<?> constr = clazz.getConstructor(argTypes);
classLoader = (WebappClassLoaderBase) constr.newInstance(args);
return classLoader;
}
复制代码
该方法会实例化一个ParallelWebappClassLoader实例,并且传递了sharedLoader作为其父亲加载器
。
代码阅读到这里,已经基本清楚了Tomcat中ClassLoader的总体结构,总结如下: 在Tomcat存在common、cataina、shared三个公共的classloader,默认情况下,这三个classloader其实是同一个,都是common classloader,而针对每个webapp,也就是context(对应代码中的StandardContext类),都有自己的WebappClassLoader实例来加载每个应用自己的类,该类加载实例的parent即是Shared ClassLoader。
这样前面关于tomcat的类加载层次应该就清楚起来了。
3.3 tomcat类加载器的加载过程
前面介绍了tomcat类加载器的创建及层次,下面进入本篇最后一点内容,这些类加载器是怎样加载类的呢?
所以重点看ParallelWebappClassLoader
(看名字就是并行的WebappClassLoader,具体的差异就不做研究了)。
ParallelWebappClassLoader的loadClass是在其父类WebappClassLoaderBase中实现的:
第一步:
首先调用findLoaderClass0() 方法检查WebappClassLoader中是否加载过此类。
WebappClassLoader 加载过的类都存放在 resourceEntries 缓存中。protected final Map<String, ResourceEntry> resourceEntries = new ConcurrentHashMap<>();
第二步:
如果第一步没有找到,则继续检查JVM虚拟机中是否加载过该类。
调用ClassLoader的findLoadedClass()方法检查。
第三步:
如果前两步都没有找到,则使用系统类加载该类(也就是当前JVM的ClassPath)。为了防止覆盖基础类实现,这里会判断class是不是JVMSE中的基础类库中类。
protected ClassLoader getJavaseClassLoader() {
return javaseClassLoader;
}
复制代码
第四步:
先判断是否设置了delegate属性,设置为true,那么就会完全按照JVM的"双亲委托"机制流程加载类。
若是默认的话,是先使用WebappClassLoader自己处理加载类的。当然,若是委托了,使用双亲委托亦没有加载到class实例,那还是最后使用WebappClassLoader加载。
第五步:
若是没有委托,则默认会首次使用WebappClassLoader来加载类。通过自定义findClass定义处理类加载规则。
findClass()会去Web-INF/classes 目录下查找类。
去阅读里面的代码,里面有这样一个方法:
@Override
public WebResource getClassLoaderResource(String path) {
return getResource("/WEB-INF/classes" + path, true, true);
}
复制代码
第六步:
若是WebappClassLoader在/WEB-INF/classes、/WEB-INF/lib下还是查找不到class,那么无条件强制委托给System、Common类加载器去查找该类。
最后借tomcat官网上的话总结一下:
Web应用类加载器默认的加载顺序是:
(1).先从缓存中加载;
(2).如果没有,则从JVM的Bootstrap类加载器加载;
(3).如果没有,则从当前类加载器加载(按照WEB-INF/classes、WEB-INF/lib的顺序);
(4).如果没有,则从父类加载器加载,由于父类加载器采用默认的委派模式,所以加载顺序是AppClassLoader、Common、Shared。
tomcat提供了delegate属性用于控制是否启用java委派模式,默认false(不启用),当设置为true时,tomcat将使用java的默认委派模式,这时加载顺序如下:
(1).先从缓存中加载;
(2).如果没有,则从JVM的Bootstrap类加载器加载;
(3).如果没有,则从父类加载器加载,加载顺序是AppClassLoader、Common、Shared。
(4).如果没有,则从当前类加载器加载(按照WEB-INF/classes、WEB-INF/lib的顺序);
参考文章
Tomcat架构解析
tomcat启动流程
Tomcat源码分析 -- Tomcat类加载器