【Tomcat源码分析】揭秘 Tomcat 启动-初篇

2024-09-13 18:47:37 浏览数 (5)

前言

说到 Tomcat 的启动,我们常需运行“tomcat/bin/startup.sh”脚本,但脚本内容究竟为何?不妨一探究竟。

启动脚本

startup.sh 脚本

代码语言:javascript复制
#!/bin/sh
os400=false
case "`uname`" in
OS400*) os400=true;;
esac

# 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 "$@"

该脚本中,两个重要变量

  • “PRGDIR”指向脚本所在路径,
  • “EXECUTABLE”指向“catalina.sh”脚本名称。其中,最关键代码“exec "EXECUTABLE" start "$@"”执行了“catalina.sh”脚本,并传入“start”参数。

catalina.sh 脚本

接下来,让我们深入探究“catalina.sh”脚本的实现。

代码语言:javascript复制
elif [ "$1" = "start" ] ; then

  if [ ! -z "$CATALINA_PID" ]; then
    if [ -f "$CATALINA_PID" ]; then
      if [ -s "$CATALINA_PID" ]; then
        echo "Existing PID file found during start."
        if [ -r "$CATALINA_PID" ]; then
          PID=`cat "$CATALINA_PID"`
          ps -p $PID >/dev/null 2>&1
          if [ $? -eq 0 ] ; then
            echo "Tomcat appears to still be running with PID $PID. Start aborted."
            echo "If the following process is not a Tomcat process, remove the PID file and try again:"
            ps -f -p $PID
            exit 1
          else
            echo "Removing/clearing stale PID file."
            rm -f "$CATALINA_PID" >/dev/null 2>&1
            if [ $? != 0 ]; then
              if [ -w "$CATALINA_PID" ]; then
                cat /dev/null > "$CATALINA_PID"
              else
                echo "Unable to remove or clear stale PID file. Start aborted."
                exit 1
              fi
            fi
          fi
        else
          echo "Unable to read PID file. Start aborted."
          exit 1
        fi
      else
        rm -f "$CATALINA_PID" >/dev/null 2>&1
        if [ $? != 0 ]; then
          if [ ! -w "$CATALINA_PID" ]; then
            echo "Unable to remove or write to empty PID file. Start aborted."
            exit 1
          fi
        fi
      fi
    fi
  fi

  shift
  touch "$CATALINA_OUT"
  if [ "$1" = "-security" ] ; then
    if [ $have_tty -eq 1 ]; then
      echo "Using Security Manager"
    fi
    shift
    eval $_NOHUP ""$_RUNJAVA"" ""$LOGGING_CONFIG"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS 
      -classpath ""$CLASSPATH"" 
      -Djava.security.manager 
      -Djava.security.policy==""$CATALINA_BASE/conf/catalina.policy"" 
      -Dcatalina.base=""$CATALINA_BASE"" 
      -Dcatalina.home=""$CATALINA_HOME"" 
      -Djava.io.tmpdir=""$CATALINA_TMPDIR"" 
      org.apache.catalina.startup.Bootstrap "$@" start 
      >> "$CATALINA_OUT" 2>&1 "&"

  else
    eval $_NOHUP ""$_RUNJAVA"" ""$LOGGING_CONFIG"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS 
      -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."

该脚本虽然冗长,但我们只需关注“start”参数的处理逻辑。当传入“start”参数时,脚本会执行最后一行代码“org.apache.catalina.startup.Bootstrap "$@" start”,即调用“Bootstrap”类的“main”方法并传递“start”参数。接下来,让我们深入了解“Bootstrap”类的“main”方法是如何实现的。

Bootstrap.main

首先,我们进入“main”方法。

代码语言:javascript复制
public static void main(String args[]) {
    System.err.println("Have fun and Enjoy! cxs");

    // daemon 就是 bootstrap
    if (daemon == null) {
        Bootstrap bootstrap = new Bootstrap();
        try {
            //类加载机制我们前面已经讲过,在这里就不在重复了
            bootstrap.init();
        } catch (Throwable t) {
            handleThrowable(t);
            t.printStackTrace();
            return;
        }
        daemon = bootstrap;
    } else {
        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);// bootstrap 和 Catalina 一脉相连, 这里设置, 方法内部设置 Catalina 实例setAwait方法
            daemon.load(args);// args 为 空,方法内部调用 Catalina 的 load 方法.
            daemon.start();// 相同, 反射调用 Catalina 的 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);
    }
}

让我们关注“bootstrap.init()”代码段。

代码语言:javascript复制
public void init() throws Exception {

// 类加载机制我们前面已经讲过,在这里就不在重复了
initClassLoaders();

Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);

// 反射方法实例化Catalina
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();


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

// 引用Catalina实例
catalinaDaemon = startupInstance;
}

代码通过反射机制实例化“Catalina”类,并将实例引用赋值给“catalinaDaemon”。接下来,让我们关注“daemon.load(args);”部分。

代码语言:javascript复制
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);
    //通过反射调用Catalina的load()方法
    method.invoke(catalinaDaemon, param);

}

Catalina.load

我们发现“daemon.load(args)”实际上是通过反射机制调用“Catalina”类的“load()”方法。接下来,让我们进入“Catalina”类的“load()”方法。

代码语言:javascript复制
public void load() {

    initDirs();

    // 初始化jmx的环境变量
    initNaming();

    // Create and execute our Digester
    // 定义解析server.xml的配置,告诉Digester哪个xml标签应该解析成什么类
    Digester digester = createStartDigester();

    InputSource inputSource = null;
    InputStream inputStream = null;
    File file = null;
    try {

      // 首先尝试加载conf/server.xml,省略部分代码......
      // 如果不存在conf/server.xml,则加载server-embed.xml(该xml在catalina.jar中),省略部分代码......
      // 如果还是加载不到xml,则直接return,省略部分代码......

      try {
          inputSource.setByteStream(inputStream);

          // 把Catalina作为一个顶级实例
          digester.push(this);

          // 解析过程会实例化各个组件,比如Server、Container、Connector等
          digester.parse(inputSource);
      } catch (SAXParseException spe) {
          // 处理异常......
      }
    } finally {
        // 关闭IO流......
    }

    // 给Server设置catalina信息
    getServer().setCatalina(this);
    getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
    getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());

    // Stream redirection
    initStreams();

    // 调用Lifecycle的init阶段
    try {
        getServer().init();
    } catch (LifecycleException e) {
        // ......
    }

    // ......

}

Server 初始化

在“Catalina”类的“load()”方法中,我们发现了“getServer.init()”方法,顾名思义,它是启动“Server”的初始化方法,而“Server”是图中最外层的容器。因此,让我们深入研究“getServer.init()”方法,即“LifecycleBase.init()”方法。该方法是一个模板方法,定义了一个算法框架,将一些细节算法留给子类实现。接下来,我们分析“LifecycleBase.init()”方法。

代码语言:javascript复制
@Override
public final synchronized void init() throws LifecycleException {
    // 1
    if (!state.equals(LifecycleState.NEW)) {
        invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
    }
    // 2
    setStateInternal(LifecycleState.INITIALIZING, null, false);

    try {
        // 模板方法
        /**
         * 采用模板方法模式来对所有支持生命周期管理的组件的生命周期各个阶段进行了总体管理,
         * 每个需要生命周期管理的组件只需要继承这个基类,
         * 然后覆盖对应的钩子方法即可完成相应的声明周期阶段的管理工作
         */
        initInternal();
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        setStateInternal(LifecycleState.FAILED, null, false);
        throw new LifecycleException(
            sm.getString("lifecycleBase.initFail",toString()), t);
    }

    // 3
    setStateInternal(LifecycleState.INITIALIZED, null, false);
}

“Server”的实现类为“StandardServer”,我们来分析一下“StandardServer.initInternal()”方法。该方法用于对“Server”进行初始化,关键部分在于最后对“services”的循环操作,对每个“service”调用“init”方法。

[注:此处只粘贴代码片段]

“StandardServer.initInternal()”

代码语言:javascript复制
@Override
protected void initInternal() throws LifecycleException {
    super.initInternal();

    // Initialize our defined Services
    for (int i = 0; i < services.length; i  ) {
        services[i].init();
    }
}

调用“Service”子容器的“init”方法,使“Service”组件完成初始化。需要注意的是,同一个“Server”下面可能存在多个“Service”组件。

Service 初始化

“StandardService”和“StandardServer”都继承自“LifecycleMBeanBase”,因此公共的初始化逻辑相同,这里不做过多介绍。我们直接看“initInternal”方法:

“StandardService.initInternal()”

代码语言:javascript复制
protected void initInternal() throws LifecycleException {

    // 往jmx中注册自己
    super.initInternal();

    // 初始化Engine
    if (engine != null) {
        engine.init();
    }

    // 存在Executor线程池,则进行初始化,默认是没有的
    for (Executor executor : findExecutors()) {
        if (executor instanceof JmxEnabled) {
            ((JmxEnabled) executor).setDomain(getDomain());
        }
        executor.init();
    }

    mapperListener.init();

    // 初始化Connector,而Connector又会对ProtocolHandler进行初始化,开启应用端口的监听,
    synchronized (connectorsLock) {
        for (Connector connector : connectors) {
            try {
                connector.init();
            } catch (Exception e) {
                // 省略部分代码,logger and throw exception
            }
        }
    }
}
  • 首先,将“StandardService”注册到 JMX 中。
  • 然后,初始化“Engine”,而“Engine”在初始化过程中会初始化“Realm”(权限相关的组件)。
  • 如果存在“Executor”线程池,还会进行“init”操作,该“Excecutor”是 Tomcat 的接口,继承自“java.util.concurrent.Executor”和“org.apache.catalina.Lifecycle”。
  • 最后,初始化“Connector”连接器,默认包含“http1.1”和“ajp”连接器,而“Connector”初始化过程中会对“ProtocolHandler”进行初始化,开启应用端口监听,后面会详细分析。

Engine 初始化

以下是“StandardEngine”初始化的代码:

代码语言:javascript复制
@Override
protected void initInternal() throws LifecycleException {
    getRealm();
    super.initInternal();
}

public Realm getRealm() {
    Realm configured = super.getRealm();
    if (configured == null) {
        configured = new NullRealm();
        this.setRealm(configured);
    }
    return configured;
}

“StandardEngine”继承自“ContainerBase”,而“ContainerBase”重写了“initInternal()”方法,用于初始化“start”和“stop”线程池,该线程池具有以下特点:

  1. 核心线程数和最大线程数相等,默认为 1。
  2. 允许核心线程在超时未获取到任务时退出线程。
  3. 线程获取任务的超时时间为 10 秒,也就是说所有线程(包括核心线程),超过 10 秒未获取到任务,就会被销毁。

这么做的目的是因为该线程池只需要在容器启动和停止时发挥作用,没有必要时时刻刻处理任务队列。

以下是“ContainerBase”的代码:

代码语言:javascript复制
// 默认是1个线程
private int startStopThreads = 1;
protected ThreadPoolExecutor startStopExecutor;

@Override
protected void initInternal() throws LifecycleException {
    BlockingQueue<Runnable> startStopQueue = new LinkedBlockingQueue<>();
    startStopExecutor = new ThreadPoolExecutor(
            getStartStopThreadsInternal(),
            getStartStopThreadsInternal(), 10, TimeUnit.SECONDS,
            startStopQueue,
            new StartStopThreadFactory(getName()   "-startStop-"));
    // 允许core线程超时未获取任务时退出
    startStopExecutor.allowCoreThreadTimeOut(true);
    super.initInternal();
}

private int getStartStopThreadsInternal() {
    int result = getStartStopThreads();

    if (result > 0) {
        return result;
    }
    result = Runtime.getRuntime().availableProcessors()   result;
    if (result < 1) {
        result = 1;
    }
    return result;
}

“startStopExecutor”线程池的作用是在容器启动和停止时,将子容器的启动和停止操作放入线程池中进行处理。

  • 在启动时,如果发现有子容器,则会将子容器的启动操作放入线程池中处理。
  • 在停止时,也会将停止操作放入线程池中处理。

在之前的文章中我们介绍了“Container”组件,“StandardEngine”作为顶层容器,它的直接子容器是“StandardHost”。但是,在对“StandardEngine”代码的分析中,我们并没有发现它会对子容器“StandardHost”进行初始化操作。 “StandardEngine”不按套路出牌,而是将初始化过程放在启动阶段。

个人认为,“Host”、“Context”、“Wrapper”这些容器与具体的 Web 应用相关联,初始化过程会更加耗时。因此,在启动阶段使用多线程完成初始化和启动生命周期,否则,像顶层的“Server”、“Service”等组件需要等待“Host”、“Context”、“Wrapper”完成初始化才能结束初始化流程,整个初始化过程是具有传递性的。

“Connector”的初始化将在后面专门的“Connector”文章中讲解。

结束

至此,整个初始化过程便告一段落。整个初始化过程,由父组件控制子组件的初始化,一层层往下传递,直到最后全部初始化完成。下图描述了整体的传递流程。

默认情况下,Server 只有一个 Service 组件,Service 组件先后对 Engine 和 Connector 进行初始化。而 Engine 组件并不会在初始化阶段对子容器进行初始化,Host、Context、Wrapper 容器的初始化是在启动阶段完成的。Tomcat 默认会启用 HTTP1.1 和 AJP 的 Connector 连接器,这两种协议默认使用 Http11NioProtocol 和 AJPNioProtocol 进行处理。

好了,本章节到此告一段落。希望对你有所帮助,祝学习顺利。

0 人点赞