Tomcat的启动与关闭:详解启动类Bootstrap和Catalina,彻底搞懂catalina.home和catalina.base的区别和作用范围

Posted 徐同学呀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Tomcat的启动与关闭:详解启动类Bootstrap和Catalina,彻底搞懂catalina.home和catalina.base的区别和作用范围相关的知识,希望对你有一定的参考价值。

一、前言

Tomcat启动其实和普通的java程序一样,都是通过一个main方法运行起来的,然后起几个异步线程不断循环监听,保证main主线程一直运行不退出。org.apache.catalina.startup.Bootstrap是Tomcat的启动类,它有一个main方法,可以传入一些控制指令如startstop等,还可以传一些指定配置路径的参数如-config等。也可以不传任何参数,默认start,配置信息默认就从catalina.base中获取,如catalina.base/conf/server.xmlcatalina.base/conf/context.xml等。

二、设置catalina.home和catalina.base

1、二者区别和作用范围

catalina.homecatalina.base是Tomcat的两个非常重要的路径:

  • catalina.home是Tomcat的安装目录,一般包含bin目录和lib目录,bin目录下有控制Tomcat的脚本,如catalin.sh等以及启动类jar包Bootstrap.jarlib目录下是Tomcat启动运行需要的jar包(安装版的Tomcat是把源码打包成多个单独的jar)。

  • catalina.baseweb项目部署目录,也是工作目录,包含conflogswebapps等目录。conf中有一些默认配置如server.xmlcontext.xmllogging.properties以及全局web.xml等;logs就是web程序运行时产生的日志,可以在server.xml中配置生成日志的路径和文件名;webapps就是web项目部署的地方,可以在server.xml<Host>节点配置appBase的路径,默认为webappswebapps目录下的web项目就是一个个context,默认都是根据conf/context.xml解析context中的容器和组件,也可以单独指定,如web项目目录下可以包含META-INF/context.xml

  • Tomcat会想方设法设置catalina.home,如果没有在系统环境或者vm options指定就根据用户当前工作目录(user.dir)设置合适的路径,catalina.base也没有指定的话,就直接把catalina.home赋值给catalina.base

2、bin/catalina.sh中设置catalina.home和catalina.base

启动Tomcat的方式有两种,一种用命令脚本启动(安装包方式),一种是直接运行Bootstrap.main源码方式)。不过这两种启动方式,原理是一样的,都是通过vm optionsProgram arguments 指定一些启动参数,然后调用Bootstrap.main

bin/catalina.sh是控制Tomcat的脚本,可以传递一些runstartstopdebug等指令控制Tomcat的启动过程。有两个便捷脚本startup.shshutdown.sh,都是调用的catalin.sh脚本传递startstop指令。

catalina.sh的最终目的就是生成一个包含各种vm optionsProgram arguments ,并将这些参数传递给启动类Bootstrap的命令。(因为脚本源码较多,只截取关键部分)

catalina.sh Bootstrap start
如何确定catalina.homecatalina.base的值呢?

  • 如果没有在系统环境里设置Tomcat的安装目录即CATALINA_HOME,则设置为catalina.sh脚本当前目录(/bin)的上一级目录为CATALINA_HOME
  • 如果没有在系统环境里设置工作目录即 CATALINA_BASE,则将 CATALINA_HOME 赋值给 CATALINA_BASE
    catalina.sh 如何确定catalina.home和catalina.base

3、Bootstrap中确定catalina.home和catalina.base

不管用catalina.sh脚本启动Tomcat还是直接运行org.apache.catalina.startup.Bootstrap#main,都是可以在vm options中设置-Dcatalina.home-Dcatalina.base

Bootstrapstatic块里会对catalinaBaseFilecatalinaHomeFile进行赋值:

  • 首先获取用户当前工作目录,一般就是项目的路径。
  • 获取-Dcatalina.home值,如果不为空,则new一个File,并调用getCanonicalFile获取抽象路径名的规范形式。(getCanonicalFile可以根据操作系统,解析得到规范的唯一路径名,如../ ./getCanonicalFile可以正确解析,而getAbsoluteFile不会。)
  • 如果-Dcatalina.home为空,判断用户工作目录下是否有启动类bootstrap.jar,有则将用户工作目录的上一级目录设置为catalina.home
  • -Dcatalina.home为空,用户工作目录下也没有bootstrap.jar,则catalina.home设置为用户工作目录。
  • 获取-Dcatalina.base值设置catalina.base,如果为空,则将catalina.home赋值给catalina.base
private static final File catalinaBaseFile;
private static final File catalinaHomeFile;
static {
    // Will always be non-null
    // 获取用户当前工作目录,不知道是哪里的可以debug一下
    String userDir = System.getProperty("user.dir");

    // 1. 首先从vm options 的-Dcatalina.home中获取catalina.home的路径
    // Home first
    String home = System.getProperty(Constants.CATALINA_HOME_PROP);
    File homeFile = null;

    if (home != null) {
        File f = new File(home);
        try {
            // 获取抽象路径名的规范形式,可以根据操作系统,解析得到规范的唯一路径名
            // 如../ ./ 等getCanonicalFile可以正确解析,而getAbsoluteFile不会
            homeFile = f.getCanonicalFile();
        } catch (IOException ioe) {
            homeFile = f.getAbsoluteFile();
        }
    }
    if (homeFile == null) {
        // First fall-back. See if current directory is a bin directory
        // in a normal Tomcat install
        // 2. vm options没有指定catalina.home,且userDir目录下有启动类bootstrap.jar
        // 则userDir的上一级目录设置为 catalina.home
        File bootstrapJar = new File(userDir, "bootstrap.jar");
        if (bootstrapJar.exists()) {
            File f = new File(userDir, "..");
            try {
                homeFile = f.getCanonicalFile();
            } catch (IOException ioe) {
                homeFile = f.getAbsoluteFile();
            }
        }
    }

    if (homeFile == null) {
        // Second fall-back. Use current directory
        // 3. userDir下没有bootstrap.jar,且homeFile依然为null,
        // 则将catalina.home直接设置为userDir
        File f = new File(userDir);
        try {
            homeFile = f.getCanonicalFile();
        } catch (IOException ioe) {
            homeFile = f.getAbsoluteFile();
        }
    }

    catalinaHomeFile = homeFile;
    System.setProperty(
            Constants.CATALINA_HOME_PROP, catalinaHomeFile.getPath());

    // Then base
    // 4. 从 vm options 的-Dcatalina.base 获取catalina.base的路径
    String base = System.getProperty(Constants.CATALINA_BASE_PROP);
    if (base == null) {
        // 没有指定-Dcatalina.base,则将catalina.home赋值给catalina.base
        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());
}

三、Bootstrap.main详解

org.apache.catalina.startup.Bootstrap是一个启动引导类,本身没有太多启动关闭细节的实现,而是通过加载org.apache.catalina.startup.Catalina,对Catalina发号施令。需要注意的是,Caralina是Tomcat自定义的类加载器加载实例化的,Bootstrap通过反射调用Catalinastartstop等方法。这样的组合目的,官方的意思是:

The purpose of this roundabout approach is to keep the Catalina internal classes (and any other classes they depend on, such as an XML parser) out of the system class path and therefore not visible to application level classes.

这种迂回方法的目的是将Catalina内部类(以及它们所依赖的任何其他类,如XML解析器)排除在系统类路径之外,因此对应用程序级类不可见。

个人理解:首先保证了CatalinaBootstrap的隔离和实现细节不可见,其次二者可以单独打包,像组件一样进行组装和拆除。

Bootstrap.main

public static void main(String args[]) {
    // Tomcat8.5.9还没有加锁的
    // 这里加锁 应该是为了防止多次初始化吧
    synchronized (daemonLock) {
        if (daemon == null) {
            // Don't set daemon until init() has completed
            Bootstrap bootstrap = new Bootstrap();
            try {
                // 1、初始化类加载器,并初始化calatina
                bootstrap.init();
            } catch (Throwable t) {
                handleThrowable(t);
                t.printStackTrace();
                return;
            }
            daemon = bootstrap;
        } else {
            // 设置当前线程的ContextClassLoader为catalinaLoader,
            // 确保后续一些类使用正确的类加载加载,破坏双亲委派的一贯做法。
            Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
        }
    }

    try {
        String command = "start";
        // 默认command=start,如果args有参数,必须把start放在最后
        // 如 -config xxx start
        if (args.length > 0) {
            command = args[args.length - 1];
        }
        // 省略了不太重要的startd、stopd、configtest指令
       if (command.equals("start")) {
            // 以下三个方法都是反射调用calatina的方法
            daemon.setAwait(true);
            daemon.load(args);
            daemon.start();
            if (null == daemon.getServer()) {
                System.exit(1);
            }
        } else if (command.equals("stop")) {
            daemon.stopServer(args);
        } else {
            log.warn("Bootstrap: command \\"" + command + "\\" does not exist.");
        }
    } catch (Throwable t) {
        // 省略异常处理
    }
}

1、Bootstrap的初始化

main方法中首先对Bootstrap进行实例化,主要初始化了三个自定义类加载器和用catalinaLoader加载并实例化Calatina

  • 三个类自定义类加载很重要,分别为commonLoadercatalinaLoader以及sharedLoader,都是由org.apache.catalina.startup.ClassLoaderFactory创建的URLClassLoader。区别是加载类的路径不同,每种类加载都有自己的职责范围,其加载路径配置默认在conf/catalina.properties。(涉及到Tomcat的类加载机制,较为复杂,后续会单独拿出来讨论)
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=
  • 初始化好类加载器,立刻就设置当前线程的ContextClassLoadercatalinaLoader,确保后续一些Tomcat的类能够使用正确的类加载器加载,这是破坏双亲委派的一贯做法啊。

  • 最后使用catalinaLoader加载和实例化org.apache.catalina.startup.Catalina

public void init() throws Exception {

    // 初始化三个classLoader:
    // commonLoader、catalinaLoader、sharedLoader
    initClassLoaders();
    // 设置当前线程的ContextClassLoader为catalinaLoader
    Thread.currentThread().setContextClassLoader(catalinaLoader);

    SecurityClassLoad.securityClassLoad(catalinaLoader);

    // Load our startup class and call its process() method
    if (log.isDebugEnabled())
        log.debug("Loading startup class");
    // 用 catalinaLoader加载并实例化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");
    // 调用 catalina 的方法setParentClassLoader,
    // 设置catalina的parentClassLoader 为 sharedLoader
    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;
}

2、Bootstrap对Catalina发号施令

Bootstrap解析指令,反射调用对应Catalina的方法。如start指令,会顺序调用CatalinasetAwaitloadstart,可以大概看出首先解析参数,然后启动Tomcat,至于setAwait是做什么的,以及如何解析参数,启动的细节还要看Catalina

四、控制指令程序Catalina

1、加载并解析参数

load的主要作用就是解析args,获取配置文件路径(arguments),默认是catalina.base/conf/server.xml,然后解析配置并初始化Server容器。

public void load(String args[]) {

    try {
        // 处理命令参数 如-config,获取config文件路径
        if (arguments(args)) {
            load();
        }
    } catch (Exception e) {
        e.printStackTrace(System.out);
    }
}

(1)arguments解析args,首先找到-config,然后找到后面的配置文件路径,如果没有设置-config,则默认为conf/server.xml

protected String configFile = SERVER_XML;
public static final String SERVER_XML = "conf/server.xml";
protected boolean arguments(String args[]) {
    boolean isConfig = false;
    if (args.length < 1) {
        usage();
        return false;
    }
    // -config C:/study/tomcat/conf/example1.conf start
    // ["-config", "C:/study/tomcat/conf/example1.conf", "start"]
    // 获取config的文件路径
    for (String arg : args) {
        if (isConfig) {
            configFile = arg;
            isConfig = false;
        } else if (arg.equals("-config")) {
            isConfig = true;
        } else {
           // 省略部分不常用的参数解析。。。
            usage();
            return false;
        }
    }
    return true;
}

(2)解析server.xml并初始化Server。(解析server.xml较为复杂,后续会单独拿出来研究)

public void load() {

    if (loaded) {
        return;
    }
    loaded = true;

    long t1 = System.nanoTime();

    // Before digester - it may be needed
    initNaming();
    // Parse main server.xml
    // 解析server.xml
    parseServerXml(true);
    Server s = getServer();
    if (s == null) {
        return;
    }
    // 设置Server的Catalina、CatalinaHome、CatalinaBase
    getServer().setCatalina(this);
    getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
    getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());

    // Stream redirection
    // 重定向标准输出流和异常输出流
    initStreams();

    // Start the new server
    try {
        getServer().init();
    } catch (LifecycleException e) {
        if (throwOnInitFailure) {
            throw new java.lang.Error(e);
        } else {
            log.error(sm.getString("catalina.initError"), e);
        }
    }
}

load里有一个操作比较有意思,就是重定向标准输出流和异常流到内存流ByteArrayOutputStream中,使用的方式是先调用SystemLogHandler.startCapture()开启捕获,最后调用SystemLogHandler.stopCapture()停止并获取捕获到的流内容。在没有调用stopCapture之前,System.out和异常抛出都会被重定向到ByteArrayOutputStream

protected void initStreams() {
    // Replace System.out and System.err with a custom PrintStream
    // 可通过如下两个方法使用
    // SystemLogHandler.startCapture(); 开始捕获标准流
    // SystemLogHandler.stopCapture(); 停止并获取捕获的流内容
    System.setOut(new SystemLogHandler(System.out));
    System.setErr(new SystemLogHandler(System.err));
}

2、启动和停止

调用Server的一键启动方法start,可以循序渐进的启动Server容器下的所有子容器和组件。(Tomcat如何做到的一键启停,较为复杂,涉及到Tomcat的生命周期设计,后续会单独拿出来研究)

还记得setAwait设置await=true,其目的就是调用Serverawait()开启一个线程,循环监听网络(默认端口8005)中是否传来SHUTDOWN指令,如果接收到SHUTDOWN指令就退出循环,并调用Serverstopdestroy,停止并销毁Server下的所有子容器和组件。

public void start() {
    // 省略getServer()判空

    // Start the new server
    try {
        // 一键启动
        getServer().start();
    } catch (LifecycleException e) {
        log.fatal(sm.getString("catalina.serverStartFail"), e);
        try {
            getServer().destroy();
        } catch (LifecycleException e1) {
            log.debug("destroy() failed for failed Server ", e1);
        }
        return;
    }

    if (generateCode) {
        // Generate loader which will load all generated classes
        generateLoader();
    }

    // Register shutdown hook
    if (useShutdownHook) {
        // 注册 shutdown hook
        if (shutdownHook == null) {
            shutdownHook = new CatalinaShutdownHook();
        }
        Runtime.getRuntime().addShutdownHook(shutdownHook);

        // If JULI is being used, disable JULI's shutdown hook since
        // shutdown hooks run in parallel and log messages may be lost
        // if JULI's hook completes before the CatalinaShutdownHook()
        LogManager logManager = LogManager.getLogManager();
        if (logManager instanceof ClassLoaderLogManager) {
            ((ClassLoaderLogManager) logManager).setUseShutdownHook(
                    false);
        }
    }

    if (await) {
        // 调用Server的await,循环等待shutdown指令
        await();
        // 如果接收到shutdown,就结束await(),调用stop停止Tomcat
        stop();
    }
}

五、要点总结

  1. Bootstrap作为一个启动引导类,通过加载org.apache.catalina.startup.Catalina,对Catalina发号施令,其目的是保证CatalinaBootstrap的隔离和实现细节不可见,其次二者可以单独打包,像组件一样进行组装和拆除。
  2. catalina.home是Tomcat安装目录,包含Tomcat必有的bin目录和lib目录;catalina.baseweb项目部署目录,包含跟web相关的confwebapps等。如果catalina.base没有指定,则和catalina.home相同。
  3. 在Tomcat初始化,启动的过程中涉及到很多知识点,如自定义类加载器机制,server.xml的解析,Server的一键初始化,一键启停涉及到Tomcat的生命周期管理,都可以写出几篇文章来单独研究。
  4. 本篇较为基础,源码也很简单,主要了解Tomcat是如何启动,如何获取默认配置的。

Tomcat源码详细注释链接(非推广,持续更新):https://gitee.com/stefanpy/tomcat-source-code-learning

以上是关于Tomcat的启动与关闭:详解启动类Bootstrap和Catalina,彻底搞懂catalina.home和catalina.base的区别和作用范围的主要内容,如果未能解决你的问题,请参考以下文章

Tomcat的下载安装启动与关闭(ubuntu server 16.04)

TOMCAT 之 安装与启动关闭方法

Tomcat详解

JavaWeb | Tomcat详解

启动Tomcat

详解Tomcat系列-从源码分析Tomcat的启动