拆解Tomcat10: (三) 图解Tomcat的启动过程

上一篇介绍了如何在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类的方法的层级关系,通过方法名、注释等简要了解方法的作用。

简要画一下关系图,类似这样:

拆解Tomcat10: (三) 图解Tomcat的启动过程_第1张图片

​ 以上图为例,代码是逐级细化的,就像看地图一样,首先看到的是整个地球,放大一下,也就是进入了下一个层级,可以看到所有国家。针对某个国家再放大,可以看到相应的省。通过这样先整体后局部的方式把握整体层级架构,然后再按需分析一些主要的方法。在调试的时候,首先尽量少Step Into,了解完本级的大概功能后,再按需求进入子方法阅读。

​ 然后,要明确自己的目的。了解的方法的大概作用后,就要根据自己的目的进行取舍。例如此处的replace方法,已知道它的作用是替换属性中的占位符。

  • 如果是想了解框架的关键流程,一些细枝末节的辅助方法就简要过一下就行了,这样的replace方法知道作用就可以跳过了;
  • 如果想学习框架的代码技巧、算法、或者验证某功能的实现机制等,可以深入的分析一下,配合逐步调试。

按程序的执行流程来说,是对上图这棵树进行深度优先遍历的过程。但从源码分析角度,建议通过广度优先的方式,逐级进行分析。

3.按功能看处理流程

将Bootstrap类按其代码分为三部分:

  • 初始化部分,主要是初始化CATALINA_HOME 和CATALINA_BASE变量;
  • main方法部分一:创建和初始化daemon和catalinaDaemon、创建三个重要类加载器;
  • main方法部分二:控制Tomcat的启动与停止。

对应流程图如下:

拆解Tomcat10: (三) 图解Tomcat的启动过程_第2张图片

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方法,背后的逻辑比较复杂,下文单独作为一个专题讲解。

你可能感兴趣的:(Tomcat源码阅读,bootstrap,uml,经验分享)