上一篇介绍了如何在Idea中下载并调试最新的Tomcat的源码(已更新到2021.12.8日发布的Tomcat 10.0.14)。如果说Tomcat是一部处理请求的机器,想了解Tomcat是如何处理请求的,首先要了解它的内部结构,本章以Tomcat的启动为起点,开启源码学习之旅。
1. 一切从main方法开始
以main方法作为程序的起点,这可以说是大多数语言的惯例。上文已介绍了,Tomcat的启动main方法在“java/org/apache/catalina/startup/Bootstrap.java”文件中。那么我们就以这个Bootstrap类作为源码阅读的起点。
Bootstrap类的注释翻译如下:
Bootstrap类是Catalina 的引导加载程序。
该应用程序构建了一个类加载器,用于加载 Catalina 内部类(通过累积在“catalina.home”下的“server”目录中找到的所有 JAR 文件),并启动容器的常规执行。
这种迂回方法的目的是将 Catalina 内部类(以及它们所依赖的任何其他类,例如 XML 解析器)保留在系统类路径之外,因此对应用程序级类不可见。
这涉及到了此类中的两个重要变量(下文会用到):
/**
* daemon: main方法使用的守护进程对象.
*/
private static volatile Bootstrap daemon = null;
/**
* 守护程序引用的catalina对象。
*/
private Object catalinaDaemon = null;
一个是Bootstrap类型的守护进程,另一个则是Catalina ,Tomcat的核心。
但Tomcat并没有直接创建它,而是通过一个Bootstrap类型的守护程序来创建和初始化Catalina ,并管理其启动、停止等。
2. 源码分析心得
用快捷键“ctrl+shift+加号”键折叠所有代码,先整体看一下Bootstrap类的方法的层级关系,通过方法名、注释等简要了解方法的作用。
简要画一下关系图,类似这样:
以上图为例,代码是逐级细化的,就像看地图一样,首先看到的是整个地球,放大一下,也就是进入了下一个层级,可以看到所有国家。针对某个国家再放大,可以看到相应的省。通过这样先整体后局部的方式把握整体层级架构,然后再按需分析一些主要的方法。在调试的时候,首先尽量少Step Into,了解完本级的大概功能后,再按需求进入子方法阅读。
然后,要明确自己的目的。了解的方法的大概作用后,就要根据自己的目的进行取舍。例如此处的replace方法,已知道它的作用是替换属性中的占位符。
- 如果是想了解框架的关键流程,一些细枝末节的辅助方法就简要过一下就行了,这样的replace方法知道作用就可以跳过了;
- 如果想学习框架的代码技巧、算法、或者验证某功能的实现机制等,可以深入的分析一下,配合逐步调试。
按程序的执行流程来说,是对上图这棵树进行深度优先遍历的过程。但从源码分析角度,建议通过广度优先的方式,逐级进行分析。
3.按功能看处理流程
将Bootstrap类按其代码分为三部分:
- 初始化部分,主要是初始化CATALINA_HOME 和CATALINA_BASE变量;
- main方法部分一:创建和初始化daemon和catalinaDaemon、创建三个重要类加载器;
- main方法部分二:控制Tomcat的启动与停止。
对应流程图如下:
2. 初始化CATALINA_HOME 和CATALINA_BASE
首先看一下Bootstrap类,最早会通过一段代码确定CATALINA_HOME 和CATALINA_BASE两个重要值:
private static final File catalinaBaseFile;
private static final File catalinaHomeFile;
private static final Pattern PATH_PATTERN = Pattern.compile("(\"[^\"]*\")|(([^,])*)");
static {
// Will always be non-null
String userDir = System.getProperty("user.dir");
// Home first
String home = System.getProperty(Constants.CATALINA_HOME_PROP);
//省略部分代码
catalinaHomeFile = homeFile;
System.setProperty(Constants.CATALINA_HOME_PROP, catalinaHomeFile.getPath());
// Then base
String base = System.getProperty(Constants.CATALINA_BASE_PROP);
if (base == null) {
catalinaBaseFile = catalinaHomeFile;
} else {
File baseFile = new File(base);
try {
baseFile = baseFile.getCanonicalFile();
} catch (IOException ioe) {
baseFile = baseFile.getAbsoluteFile();
}
catalinaBaseFile = baseFile;
}
System.setProperty(
Constants.CATALINA_BASE_PROP, catalinaBaseFile.getPath());
}
CATALINA_HOME:代表Tomcat安装的根目录,例如/home/tomcat/apache-tomcat-9.0.10或C:\Program Files\apache-tomcat-9.0.10。
CATALINA_BASE:表示特定 Tomcat 实例的运行时配置的根。如果您想在一台机器上拥有多个 Tomcat 实例,请使用 CATALINA_BASE 属性。
默认情况下,CATALINA_HOME 和 CATALINA_BASE 指向同一目录。如果将属性设置为不同的位置,则 CATALINA_HOME 位置包含静态源,例如 .jar 文件或二进制文件。 CATALINA_BASE 位置包含配置文件、日志文件、部署的应用程序和其他运行时要求。
3. 创建并初始化守护进程daemon和catalinaDaemon
初始化完毕,接着执行的就是main方法,这个方法功能分为两部分:
synchronized (daemonLock) {
if (daemon == null) {
// 在初始化完成之前,不要对daemon赋值
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
//当作为服务正在运行时,如果调用停止方法,这将在一个新线程上进行,以确保使用正确的类加载器,防止出现未找到类的异常。
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
}
首先创建一个Bootstrap类型的对象,并调用其init()方法进行初始化,直至其初始化完毕,将其赋值给daemon对象。
重点在init方法:
public void init() throws Exception {
//创建并初始化三个ClassLoader
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");
}
//通过反射方式创建
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");
}
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;
}
3.1 创建类加载器
首先通过initClassLoaders()方法创建了三个类加载器,对应为以下的三个变量赋值:
ClassLoader commonLoader = null;
ClassLoader catalinaLoader = null;
ClassLoader sharedLoader = null;
对应的配置在conf/catalina.properties文件中,配置如下
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=
common.loader是另外两个类的父级,默认情况下,server.loader和shared.loader未作配置,返回结果同common.loader。
3.2 创建并初始化catalinaDaemon
在init()的后半部分,通过反射方式根据"org.apache.catalina.startup.Catalina"创建了Catalina对象,并调用其“setParentClassLoader”方法将sharedLoader赋值给它的parentClassLoader属性。
为什么要通过反射的方式来进行创建?
Bootstrap类的注释已经说明,主要是为了类的隔离。但这也导致了调用Catalina对象的方法也需要通过getMethod的方法获取并调用。
4. 控制Tomcat的启动状态
main方法的后半部分,通过传进来的arg值来确定执行的操作
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);
daemon.load(args);
daemon.start();
if (null == daemon.getServer()) {
System.exit(1);
}
} else if (command.equals("stop")) {
daemon.stopServer(args);
} else if (command.equals("configtest")) {
daemon.load(args);
if (null == daemon.getServer()) {
System.exit(1);
}
System.exit(0);
} else {
log.warn("Bootstrap: command \"" + command + "\" does not exist.");
}
arg是调用tomcat传的参数,若未传则默认是“start”,根据这个命令来控制tomcat的状态。
看起来简单的start、stop方法,背后的逻辑比较复杂,下文单独作为一个专题讲解。