Tomcat启动分析(一) - Bootstrap类

本系列以Tomcat 8.5.33为例分析Tomcat的启动和请求处理过程。

启动脚本

与Tomcat有关的脚本都在Tomcat主目录的bin子目录中,其中与启动有关的脚本有startup.sh、catalina.sh和setclasspath.sh。启动Tomcat时只需执行startup.sh即可,其内容如下:

# resolve links - $0 may be a softlink
PRG="$0"

while [ -h "$PRG" ] ; do
  ls=`ls -ld "$PRG"`
  link=`expr "$ls" : '.*-> \(.*\)$'`
  if expr "$link" : '/.*' > /dev/null; then
    PRG="$link"
  else
    PRG=`dirname "$PRG"`/"$link"
  fi
done

PRGDIR=`dirname "$PRG"`
EXECUTABLE=catalina.sh

# Check that target executable exists
if $os400; then
  # -x will Only work on the os400 if the files are:
  # 1. owned by the user
  # 2. owned by the PRIMARY group of the user
  # this will not work if the user belongs in secondary groups
  eval
else
  if [ ! -x "$PRGDIR"/"$EXECUTABLE" ]; then
    echo "Cannot find $PRGDIR/$EXECUTABLE"
    echo "The file is absent or does not have execute permission"
    echo "This file is needed to run this program"
    exit 1
  fi
fi

exec "$PRGDIR"/"$EXECUTABLE" start "$@"

该脚本主要做了两件事:

  • 用while循环处理符号链接,ls变量存ls命令的结果,link变量存符号链接指向的文件路径,直到PRG的值不再是符号链接为止,以此找到Tomcat的主目录;
  • exec命令执行catalina.sh,命令行参数由start和传递给startup.sh的命令行参数两部分组成。

catalina.sh利用setclasspath.sh检验JRE环境,然后根据传递给脚本的的命令行参数启动JVM,不同参数对应的分支代码如下,传给catalina.sh脚本的第一个命令行参数是给Bootstrap类传递的最后一个命令行参数。以默认情况为例,上文的startup.sh只为catalina.sh提供了start参数,则$@为空,因此会执行eval所示的代码。

if [ "$1" = "debug" ] ; then
  # 省略部分代码
elif [ "$1" = "run" ]; then
  # 省略部分代码
elif [ "$1" = "start" ] ; then
  # 省略部分代码
  shift
  touch "$CATALINA_OUT"
  if [ "$1" = "-security" ] ; then
    # 省略部分代码
  else
    eval $_NOHUP "\"$_RUNJAVA\"" "\"$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 "&"
  fi

  if [ ! -z "$CATALINA_PID" ]; then
    echo $! > "$CATALINA_PID"
  fi

  echo "Tomcat started."
elif [ "$1" = "stop" ] ; then
  # 省略部分代码
elif [ "$1" = "configtest" ] ; then
  # 省略部分代码
elif [ "$1" = "version" ] ; then
  # 省略部分代码
else
  echo "Usage: catalina.sh ( commands ... )"
  # 省略部分代码
fi

用ps aux | grep tomcat可以看到对应的执行命令如下:

/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/bin/java 
-Djava.util.logging.config.file=/Users/suntao/Documents/Workspace/apache-tomcat-8.5.33/conf/logging.properties
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Djdk.tls.ephemeralDHKeySize=2048
-Djava.protocol.handler.pkgs=org.apache.catalina.webresources
-Dorg.apache.catalina.security.SecurityListener.UMASK=0027
-Dignore.endorsed.dirs= -classpath /Users/suntao/Documents/Workspace/apache-tomcat-8.5.33/bin/bootstrap.jar:/Users/suntao/Documents/Workspace/apache-tomcat-8.5.33/bin/tomcat-juli.jar
-Dcatalina.base=/Users/suntao/Documents/Workspace/apache-tomcat-8.5.33
-Dcatalina.home=/Users/suntao/Documents/Workspace/apache-tomcat-8.5.33
-Djava.io.tmpdir=/Users/suntao/Documents/Workspace/apache-tomcat-8.5.33/temp
org.apache.catalina.startup.Bootstrap start
  • 虚拟机参数catalina.home是Tomcat的安装目录;
  • 虚拟机参数catalina.base是Tomcat的工作目录,在有多个Tomcat时可以与catalina.home不同;
  • 主类是org.apache.catalina.startup.Bootstrap,命令行参数只有一个start。

Bootstrap类

Bootstrap类用于引导Tomcat的启动过程。

成员变量

Bootstrap类的成员变量如下:

/**
 * Daemon object used by main.
 */
private static Bootstrap daemon = null;

private static final File catalinaBaseFile;
private static final File catalinaHomeFile;

/**
 * Daemon reference.
 */
private Object catalinaDaemon = null;

ClassLoader commonLoader = null;
ClassLoader catalinaLoader = null;
ClassLoader sharedLoader = null;
  • daemon引用一个Bootstrap实例,可以在启动后接受其他命令如stop;
  • catalinaHomeFile表示Tomcat的安装目录;
  • catalinaBaseFile表示Tomcat的工作目录;
  • catalinaDaemon表示正在运行的Catalina实例;
  • commonLoader、catalinaLoader和sharedLoader分别引用初始化过程中创建的三个类加载器。

静态代码块

静态代码块为catalinaHomeFile和catalinaBaseFile两个静态final成员变量赋值,其代码如下:

static {
    // Will always be non-null
    String userDir = System.getProperty("user.dir");

    // Home first
    String home = System.getProperty(Globals.CATALINA_HOME_PROP);
    File homeFile = null;

    if (home != null) {
        File f = new File(home);
        try {
            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
        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
        File f = new File(userDir);
        try {
            homeFile = f.getCanonicalFile();
        } catch (IOException ioe) {
            homeFile = f.getAbsoluteFile();
        }
    }

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

    // Then base
    String base = System.getProperty(Globals.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(
            Globals.CATALINA_BASE_PROP, catalinaBaseFile.getPath());
}
  • catalinaHomeFile变量保存Tomcat的安装目录,若系统属性catalina.home存在则使用该属性的值,否则若当前工作目录中存在bootstrap.jar文件则使用当前工作目录的父目录,否则使用当前工作目录;
  • catalinaBaseFile变量保存Tomcat的工作目录,若系统属性catalina.base存在则使用该属性的值,否则与catalinaHomeFile相同;
  • 最终,系统属性catalina.home和catalina.base都会被重新赋值。

main方法

Bootstrap类的main方法代码如下:

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);
    }

    try {
        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();
        } 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.");
        }
    } catch (Throwable t) {
        // Unwrap the Exception for clearer error reporting
        if (t instanceof InvocationTargetException &&
                t.getCause() != null) {
            t = t.getCause();
        }
        handleThrowable(t);
        t.printStackTrace();
        System.exit(1);
    }

}
  • 从脚本启动时,因为最后一个命令行参数是start,所以main方法会先执行init方法,然后执行load方法,最后是start方法;
  • 其他命令行参数同理。

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");
    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;
}

init方法主要做了以下事情:

  • 调用initClassLoaders方法初始化类加载器;
  • 将catalinaLoader设置为自己的线程上下文类加载器;
  • 利用catalinaLoader加载Catalina类并实例化对象。

初始化类加载器

在初始化类加载器过程中涉及到Catalina配置文件的加载和解析,过程如下:

  1. 若JVM参数指定了catalina.config则从该属性值表示的路径加载配置文件,若文件存在则转到4,否则转到2;
  2. 尝试加载${catalina.home}/conf/catalina.properties文件,若文件存在则转到4,否则转到3;
  3. 尝试加载${catalina.home}/bin/bootstrap.jar中的org/apache/catalina/startup/catalina.properties文件;
  4. 将配置文件中的属性和值复制到系统属性中。

默认的${catalina.home}/conf/catalina.properties配置文件部分内容如下,bootstrap.jar中的配置文件与其相似:

package.access=sun.,org.apache.catalina.,org.apache.coyote.,org.apache.jasper.,org.apache.tomcat.
package.definition=sun.,java.,org.apache.catalina.,org.apache.coyote.,\
org.apache.jasper.,org.apache.naming.,org.apache.tomcat.
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=
// 省略一些代码

initClassLoaders方法会创建三个类加载器:

  • commonLoader:父加载器是系统类加载器(ClassLoader类getSystemClassLoader方法的返回值),配置文件中以common.loader为键的属性值表示的路径会被添加到commonLoader的查找路径中;
  • catalinaLoader:父加载器是commonLoader,配置文件中以server.loader为键的属性值表示的路径会被添加到catalinaLoader的查找路径中;
  • sharedLoader:父加载器是commonLoader,配置文件中以shared.loader为键的属性值表示的路径会被添加到sharedLoader的查找路径中;
  • 若使用上述默认的配置文件,则${catalina.base}/lib目录下未打包的类和资源、${catalina.base}/lib目录下的jar、${catalina.home}/lib目录下未打包的类和资源以及${catalina.home}/lib目录下的jar都会被添加到commonLoader的搜索路径中。

有关Tomcat的类加载器的更多信息可以参考Class Loader HOW-TO。

反射实例化Catalina类

在标准启动(即从脚本启动)时,启动脚本为java命令传递了如下的参数:

-classpath /Users/suntao/Documents/Workspace/apache-tomcat-8.5.33/bin/bootstrap.jar:/Users/suntao/Documents/Workspace/apache-tomcat-8.5.33/bin/tomcat-juli.jar

可见应用类加载器只会从bootstrap.jar和tomcat-juli.jar中加载类。由于Bootstrap类存在于${catalina.home}/bin/bootstrap.jar,而Catalina类在只存在在于${catalina.home}/lib/catalina.jar,因此应用类加载器无法加载Catalina类,只能由创建的类加载器加载。

load方法

Bootstrap类的load方法代码如下,利用反射在init方法生成的Catalina类实例上调用load方法,参数即为Bootstrap的命令行参数。

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;
    }
    Method method =
        catalinaDaemon.getClass().getMethod(methodName, paramTypes);
    if (log.isDebugEnabled())
        log.debug("Calling startup class " + method);
    method.invoke(catalinaDaemon, param);
}

start方法

Bootstrap类的start方法代码如下,利用反射在init方法生成的Catalina类实例上调用Catalina类实例的start方法。

public void start()
    throws Exception {
    if( catalinaDaemon==null ) init();
    Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null);
    method.invoke(catalinaDaemon, (Object [])null);
}

你可能感兴趣的:(Tomcat启动分析(一) - Bootstrap类)