0. 写在前面
本文讲述的Tomcat相关内容,基于Tomcat9.0.34版本。
学习Tomcat源码时本人使用Idea工具,导入如下依赖来查看源码:
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.34</version>
</dependency>
1. 始于一条Shell语句
于Linux系统下启动Tomcat,只需要一条shell语句>$ startup.sh
,该shell脚本位于apache-tomcat-9.0.34in
目录下,该脚本最后一句话最为重要:
exec "$PRGDIR"/"$EXECUTABLE" start "$@"
若将其中的变量替换掉就是:
exec catalina.sh start [参数]
所以实际的启动脚本是同目录下的catalina.sh,在该脚本中,找到 $1 = start
的分支,启动Tomcat的shell基本如下:
eval $_NOHUP ""$_RUNJAVA"" ""$CATALINA_LOGGING_CONFIG"" $LOGGING_MANAGER "$JAVA_OPTS" "$CATALINA_OPTS"
-D$ENDORSED_PROP=""$JAVA_ENDORSED_DIRS""
-classpath ""$CLASSPATH""
-Dcatalina.base=""$CATALINA_BASE""
-Dcatalina.home=""$CATALINA_HOME""
-Djava.io.tmpdir=""$CATALINA_TMPDIR""
org.apache.catalina.startup.Bootstrap "$@" start
>> "$CATALINA_OUT" 2>&1 "&"
上述脚本看起来很复杂,其实就是java org.apache.catalina.startup.Bootstrap start
加上了一些启动参数。
所以,Tomcat的启动入口就是 org.apache.catalina.startup.Bootstrap
类。
2. Bootstrap类
org.apache.catalina.startup.Bootstrap
类是一个拥有public static void main(String args[])
方法的类。在启动Tomcat时,main方法所传入的参数是start。
对main方法做简化,其主要过程如下:
public static void main(String args[]) {
// 1. 生成bootstrap实例
Bootstrap bootstrap = new Bootstrap();
// 2. 调用init()方法
bootstrap.init();
// 3. 调用load()方法
bootstrap.load(args);
// 4. 调用start()方法
bootstrap.start();
.....
}
注意,main方法中处理了其他命令行参数的情况,上述简化后的过程,只针对启动时相关的代码。
init()
方法主要内容如下:
public void init() throws Exception {
// 初始化类加载器
initClassLoaders();
// 设置当前线程的ClassLoader为catalinaLoader
Thread.currentThread().setContextClassLoader(catalinaLoader);
// 通过反射,生成Catalina实例
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();
// 通过反射,设置Catalina实例的ClassLoader
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);
catalinaDaemon = startupInstance;
}
为什么需要初始化类加载器呢?实际上,Tomcat自定义了类加载器,以方便隔离不同Web应用所使用的类。这里先不做进一步说明,等介绍到加载一个Web应用时再详细阐述。
init()
方法调用完后,load()
和start()
方法实际上就是通过反射调用了Catalina的实例方法load()
和start()
,因此,实际的启动动作是在Catalina类中完成的。
3. Catalina类
Catalina类是Tomcat启动各种组件的主要场所,本文在讲述启动过程时,只关注重要组件的启动过程,而诸如JNDI、JMX或者监听器等则不予关注。
既然是启动各种组件,那么在看代码之前,有必要对Tomcat的主要组件做些介绍。
3.1 Tomcat的主要组件
有关Tomcat组件的介绍目前网络上文章很多,这里不再赘述,挑选两篇作为参考。
官方参考
不错的中文介绍
3.2 组件的生命周期
Tomcat的各个组件都遵循Tomcat生命周期定义,而作为顶层组件的Server实例管控着大多数情况下的组件生命周期。因此,想要顺畅的浏览Tomcat源码,需要对生命周期的状态流转有一定了解。
上图是Tomcat生命周期状态转移图,有了对生命周期的认识,我们只需要关注重要组件的initInternal()
,startInternal()
,stopInternal()
,destroyInternal()
等方法,就可以快速地分析组件的行为了。
3.3 Digester工具类
使用过Tomcat的开发者应该对server.xml
文件不陌生,该文件配置了Tomcat重要组件的一些参数,比如常见的监听端口,使用的字符集等。
在Tomcat启动时,会调用Catalina类的实例方法createStartDigester()
来解析server.xml
并生成各种组件实例,而完成这一过程使用的是Digester工具类。
Digester是一个类似于SAX(Simple API for XML Parsing)的基于事件驱动的xml文件解析工具。所谓事件驱动,就是当遇到某个元素节点就会做什么,而不必要将整个xml文件全部读取并构建dom树后再解析。
如何做到事件驱动呢?Digester使用Rule
来实现。Rule
就是当遇到某个元素(或者说标签)时所要遵守的规则,如创建对象、设置属性、调用某个方法等。总体而言,Digester在解析xml文件时,每当预先设定的规则匹配到某个xml元素时,就会执行规则定义的行为。
Rule
有四个重要的方法需要子类实现:
begin()
在元素匹配时调用;body()
当遇到匹配元素的嵌套内容时调用;end()
当遇到匹配元素的关闭标签时调用;finish()
当parse过程结束时调用,用来清理每个规则的使用的资源。
更为详细的Digester介绍请参考: Digester官网。Tomcat使用的Digester与组件形式的Digester稍有不同,但是总体上使用方式一致。
下面举例说明Digester是如何解析server.xml文件的:
digester.addObjectCreate("Server",
"org.apache.catalina.core.StandardServer",
"className");
digester.addSetProperties("Server");
digester.addSetNext("Server",
"setServer", // 方法名
"org.apache.catalina.Server");
digester.addObjectCreate()
方法意味着解析xml文档时,如果遇到了<Server> </Server>
中的前一个,就会创建一个org.apache.catalina.core.StandardServer
类的实例。默认的,Digester内部会维护一个Java对象栈,每当创建对象时,就会将对象压入栈顶。所以,创建的StandardServer对象目前处于栈顶。
digester.addSetProperties("Server")
表示会解析xml标签中遇到的Server元素的属性标签,并调用栈顶元素的对应setter()
方法来设置属性。
digester.addSetNext("Server","setServer","org.apache.catalina.Server")
表示遇到Server标签时,会调用栈顶的下一个元素的setServer()
方法,并将栈顶元素作为参数传入。一般这个方法用途与addChild()
类似。在上述例子中,(解析开始之前)会先将Catalina的实例压入栈,而后Server的实例入栈,这样就完成了catalina.setServer()
的调用。
3.4 Catalina类的主要作用
让我们说回Catalina类。启动过程中,Catalina类中的两个方法依次被调用:load() --> start()
。
在load()
方法中,最主要的动作是createStartDigester()
也就是通过Digester解析server.xml生成各个组件的实例。之后,在load()
中会调用getServer().init()
,因为Server包含着其他的所有组件,因此,该调用会完成所有组件的初始化操作。
在start()
方法调用时,所有组件都已经初始化完毕,因此直接调用getServer().start()
来启动所有的组件。