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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 Redis 版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
简介: --

前言


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


启动脚本


startup.sh 脚本


#!/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&nbsp;-ld&nbsp;<span class="hljs-string" style="color: #98c379; line-height: 26px;">"<span class="hljs-variable" style="color: #d19a66; line-height: 26px;">$PRG</span>"</span>
  link=`expr "$ls" : '.
-> (.)$'`
  if expr "$link" : '/.
' > /dev/null; then
    PRG="$link"
  else
    PRG=dirname&nbsp;<span class="hljs-string" style="color: #98c379; line-height: 26px;">"<span class="hljs-variable" style="color: #d19a66; line-height: 26px;">$PRG</span>"</span>/"$link"
  fi
done

PRGDIR=dirname&nbsp;<span class="hljs-string" style="color: #98c379; line-height: 26px;">"<span class="hljs-variable" style="color: #d19a66; line-height: 26px;">$PRG</span>"</span>
EXECUTABLE=catalina.sh

# Check that target executable exists
if $os400then
  # -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”脚本的实现。


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&nbsp;<span class="hljs-string" style="color: #98c379; line-height: 26px;">"<span class="hljs-variable" style="color: #d19a66; line-height: 26px;">$CATALINA_PID</span>"</span>
          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”方法。


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()”代码段。


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);”部分。


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()”方法。


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()”方法。


@Override
public final synchronized void init() throws LifecycleException {
    // 1
    if (!state.equals(LifecycleState.NEW)) {
        invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
    }
    // 2
    setStateInternal(LifecycleState.INITIALIZING, nullfalse);

    try {
        // 模板方法
        /*
         
 采用模板方法模式来对所有支持生命周期管理的组件的生命周期各个阶段进行了总体管理,
          每个需要生命周期管理的组件只需要继承这个基类,
         
 然后覆盖对应的钩子方法即可完成相应的声明周期阶段的管理工作
         */

        initInternal();
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        setStateInternal(LifecycleState.FAILED, nullfalse);
        throw new LifecycleException(
            sm.getString("lifecycleBase.initFail",toString()), t);
    }

    // 3
    setStateInternal(LifecycleState.INITIALIZED, nullfalse);
}

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


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


“StandardServer.initInternal()”


@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()”


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”初始化的代码:


@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”的代码:


// 默认是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”文章中讲解。


结束


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


alt

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



如有问题,欢迎微信搜索【码上遇见你】。




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


相关文章
|
11天前
|
前端开发 Java 应用服务中间件
【Tomcat源码分析 】"深入探索:Tomcat 类加载机制揭秘"
本文详细介绍了Java类加载机制及其在Tomcat中的应用。首先回顾了Java默认的类加载器,包括启动类加载器、扩展类加载器和应用程序类加载器,并解释了双亲委派模型的工作原理及其重要性。接着,文章分析了Tomcat为何不能使用默认类加载机制,因为它需要解决多个应用程序共存时的类库版本冲突、资源共享、类库隔离及JSP文件热更新等问题。最后,详细展示了Tomcat独特的类加载器设计,包括Common、Catalina、Shared、WebApp和Jsp类加载器,确保了系统的稳定性和安全性。通过这种设计,Tomcat实现了不同应用程序间的类库隔离与共享,同时支持JSP文件的热插拔。
【Tomcat源码分析 】"深入探索:Tomcat 类加载机制揭秘"
|
12天前
|
设计模式 应用服务中间件 容器
【Tomcat源码分析】Pipeline 与 Valve 的秘密花园
本文深入剖析了Tomcat中的Pipeline和Valve组件。Valve作为请求处理链中的核心组件,通过接口定义了关键方法;ValveBase为其基类,提供了通用实现。Pipeline则作为Valve容器,通过首尾相连的Valve链完成业务处理。StandardPipeline实现了Pipeline接口,提供了详细的Valve管理逻辑。通过对代码的详细分析,揭示了模板方法模式和责任链模式的应用,展示了系统的扩展性和模块化设计。
【Tomcat源码分析】Pipeline 与 Valve 的秘密花园
|
15天前
|
设计模式 人工智能 安全
【Tomcat源码分析】生命周期机制 Lifecycle
Tomcat内部通过各种组件协同工作,构建了一个复杂的Web服务器架构。其中,`Lifecycle`机制作为核心,管理组件从创建到销毁的整个生命周期。本文详细解析了Lifecycle的工作原理及其方法,如初始化、启动、停止和销毁等关键步骤,并展示了LifecycleBase类如何通过状态机和模板模式实现这一过程。通过深入理解Lifecycle,我们可以更好地掌握组件生命周期管理,提升系统设计能力。欢迎关注【码上遇见你】获取更多信息,或搜索【AI贝塔】体验免费的Chat GPT。希望本章内容对你有所帮助。
|
28天前
|
网络协议 Java 应用服务中间件
Tomcat源码分析 (一)----- 手撕Java Web服务器需要准备哪些工作
本文探讨了后端开发中Web服务器的重要性,特别是Tomcat框架的地位与作用。通过解析Tomcat的内部机制,文章引导读者理解其复杂性,并提出了一种实践方式——手工构建简易Web服务器,以此加深对Web服务器运作原理的认识。文章还详细介绍了HTTP协议的工作流程,包括请求与响应的具体格式,并通过Socket编程在Java中的应用实例,展示了客户端与服务器间的数据交换过程。最后,通过一个简单的Java Web服务器实现案例,说明了如何处理HTTP请求及响应,强调虽然构建基本的Web服务器相对直接,但诸如Tomcat这样的成熟框架提供了更为丰富和必要的功能。
|
4月前
|
前端开发 Java 应用服务中间件
|
4月前
|
XML Java 应用服务中间件
SpringBoot配置外部Tomcat项目启动流程源码分析(长文)
SpringBoot配置外部Tomcat项目启动流程源码分析(长文)
361 0
|
存储 缓存 前端开发
07.Tomcat源码分析——类加载体系
由于在生产环境中,Tomcat一般部署在Linux系统下,所以本文将以 startup.sh shell脚本为准,对Tomcat的启动进行分析。
51 0
07.Tomcat源码分析——类加载体系
|
前端开发 Java 应用服务中间件
TOMCAT 源码分析 -- 一次请求
TOMCAT 源码分析 -- 一次请求
91 0
|
Java 应用服务中间件
TOMCAT 源码分析 -- 构建环境
TOMCAT 源码分析 -- 构建环境
89 0
|
监控 前端开发 Java
TOMCAT 源码分析 -- 启动(下)
TOMCAT 源码分析 -- 启动
81 0