目 录CONTENT

文章目录

Tomcat

FatFish1
2025-03-04 / 0 评论 / 0 点赞 / 23 阅读 / 0 字 / 正在检测是否收录...

tomcat是servlet运行的容器,它承担的作用是为每个servlet暴露对应的端口,并将接收到的请求分发到这些servlet中

spring-boot有自主集成的tomcat,不使用spring-boot的项目中tomcat往往是需要自己进行集成的。tomcat的启动入口是catalina.sh

tomcat配置文件

server.xml

<?xml version='1.0' encoding='utf-8'?>
<Server port="-1" shutdown="Rea!!yC0mplexW0rd">
    <!-- SecAs: TOMCAT_CONF 2.2.6  -->
    <Listener className="org.apache.catalina.startup.VersionLoggerListener"/>
    <!-- SecAs: SEC-APP-WebApp-100: need Security Listener -->
    <Listener className="org.apache.catalina.security.SecurityListener" checkedOsUsers="root" minimumUmask="0007"/>
    <!--APR library loader. Documentation at /docs/apr.html -->
    <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on"/>
    <!--Initialize Jasper prior to webapps are loaded. Documentation at /docs/jasper-howto.html -->
    <!-- Prevent memory leaks due to use of particular java/javax APIs-->
    <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"/>
    <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener"/>
    <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/>

    <Service name="Catalina">
        <!-- SecAs: TOMCAT_CONF 2.2.7 PostSize and  HeaderSize-->
        <!-- SecAs: TOMCAT_CONF 2.2.8 connectionTimeout should not be 0 or -1 -->
        <!-- SecAs: TOMCAT_CONF 2.2.15 no X-Powered-By. server=.. -->
        <!-- SecAs: Web_SPEC 6.6 Encoding -->
        <Connector port="8443" protocol=...>
            <SSLHostConfig sessionCacheSize="1000" ...>
                <Certificate certificateFile=.../>
            </SSLHostConfig>
        </Connector>

        <Engine name="Catalina" defaultHost="localhost">
            <!-- SecAs: HWCLOUDS: Disable range request(403) -->
            <Valve className=.../>

            <!-- SecAs: TOMCAT_CONF 2.1.5 disable autoDeploy -->
            <!-- SecAs: TOMCAT_CONF 2.2.4 customize error-page -->
            <Host name="localhost" appBase=...>

                <!-- SingleSignOn valve, share authentication between web applications
                     Documentation at: /docs/config/valve.html -->
                <!--Valve className="org.apache.catalina.authenticator.SingleSignOn" /-->

                <!-- SecAs: TOMCAT_CONF 2.2.19 Do not resolve host in log valve(resolveHosts is false by default )-->
                <!-- SecAs: TOMCAT_CONF 2.5.1 Enable Log-->
                <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
                    prefix="localhost_access_log." suffix=".txt"
                    pattern="%h %l %u %t &quot;%r&quot; %s %b" />

                <!--SecAs: This is an example. It MUST be deleted before releasing -->
                <!--SecAs: TOMCAT_CONF 2.2.12/WEB_SPEC 5.2: cookie httponly. useHttpOnly default is true -->
                <!--<Context path="" docBase="ROOT" reloadable="false" crossContext="false" allowLinking="false" useHttpOnly="true"/>-->
                <Context path=.../>
                </Context>
            </Host>

            <!-- SecAs: TOMCAT_CONF 2.2.3:  disable Symbolic links(disabled by default)-->
        </Engine>
    </Service>
</Server>

可以看出来是以server、service、connector、engine、host、context等组织的,即符合tomcat组件逻辑顺序

localhost_access_log.txt文件配置

其中在<Host>标签中,有一个<Value>子标签,其中配置的是localhost_access_log.txt,它记录了请求经过Tomcat调用到服务的记录

<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
       prefix="localhost_access_log." suffix=".txt"
       pattern="%h %l %u %t &quot;%r&quot; %s %b" />
%a	Remote IP address  远程ip
%A	Local IP address  本地ip
%b	Bytes sent, excluding HTTP headers, or ‘-‘ if zero 发送字节数,不包括HTTP头,没发送为-
%B	Bytes sent, excluding HTTP headers 发送字节数,不包括HTTP头
%h	Remote host name (or IP address if enableLookups for the connector is false) 远程主机名
%H	Request protocol 请求协议
%l	Remote logical username from identd (always returns ‘-‘) 远程逻辑从identd的用户名,总返回-
%m	Request method (GET, POST, etc.) 请求方法
%p	Local port on which this request was received 本地端口
%q	Query string (prepended with a ‘?’ if it exists) 查询字符串,如果存在则在前面加个?
%r	First line of the request (method and request URI) 第一行的请求URI
%s	HTTP status code of the response 响应的HTTP状态码
%S	User session ID  用户会话ID
%t	Date and time, in Common Log Format  日期和时间,通用格式
%u	Remote user that was authenticated (if any), else ‘-‘  远程用户身份验证
%U	Requested URL path  请求的URL路径
%v	Local server name  本地服务器名
%D	Time taken to process the request, in millis  处理请求耗时,毫秒
%T	Time taken to process the request, in seconds  处理请求耗时,秒
%F	Time taken to commit the response, in millis  发送响应耗时,毫秒
%I	Current request thread name (can compare later with stacktraces)  当前请求的线程名称

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.5" xmlns=http://java.sun.com/xml/ns/javaee
        xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
        xsi:schemaLocation="http://java.sun. com/
        xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <display-name>Springmvc</display-name>

    <!-- 使⽤ContextLoaderListener配置时,需要告诉它Spring配置⽂件的位置 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
            <param-value>classpath:applicationContext.xml</paramvalue>
    </context-param>

    <!-- SpringMVC的前端控制器 -->
    <!-- 当DispatcherServlet载⼊后,它将从⼀个XML⽂件中载⼊Spring的应⽤上下⽂,该XML⽂件的名字取决于<servlet-name> -->
    <!-- 这⾥DispatcherServlet将试图从⼀个叫作Springmvc-servlet.xml的⽂件中载⼊应⽤上下⽂,其默认位于WEB-INF⽬录下 -->
    <servlet>
        <servlet-name>Springmvc</servlet-name>
        <servlet-class>org.Springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Springmvc</servlet-name>
        <url-pattern>*.htm</url-pattern>
    </servlet-mapping>

    <!-- 配置上下文载⼊器 -->
    <!-- 上下文载入器载入除DispatcherServlet载⼊的配置文件之外的其他上下文配置⽂件 -->
    <!-- 最常用的上下文载⼊器是⼀个Servlet监听器,其名称为ContextLoaderListener -->
    <listener>
        <listener-class>org.Springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

</web-app>

web.xml的核心配置在于四个点:

  • <context-param>中的contextConfigLocation配置。

  • <servlet>配置:配置请求拦截器

  • <listener>配置:配置了监听器

  • <servlet-mapping>标签:建立请求url和servlet之间的映射关系

<context-param> 标签

在springMVC架构中,最核心的是contextConfigLocation配置,这个参数就是使Web与Spring的配置⽂件相结合的⼀个关键配置

<servlet>标签

servlet-name、servlet-class、load-on-startup标签共同配置了请求拦截器,也就是servlet,其中load-on-startup配置是优先级。不同的架构下,可以理解为servlet是一个物流系统,拿到请求,做不同的包装,分发给对应的功能模块。

例如,SpringMVC架构下是DispatcherServlet,而华为serviceComb架构下是RestServlet

后面还有新的标签支持:

async-supported:支持异步处理

<listener>标签

通过listener-class标签配置监听器。

该标签配置的是监听器,tomcat解析web.xml后会发ServletContextEvent事件,监听器可以监听事件并自主决定servlet如何启动。监听器作用于服务启动时,而拦截器作用于服务工作时也就是接收到请求时。

监听器可以有一个也可以有多个,比如springMVC的ContextLoaderListener,比如华为serviceComb的SubRestServletContextListener和EdgeListener

<servlet-mapping>标签

通过servlet-nameurl-pattern两个配置项建立请求url和servlet之间的映射关系。

servlet-name必须是servlet标签中配置的servlet,当请求格式于url-pattern一致时,使用对应的servlet处理该请求。

catalina详解

一般web项目不管是使用tomcat的start.sh还是自己编写的start.sh,都是调用tomcat的启动文件catalina.sh进行启动的

分析下catalina.sh到底讲了什么

cygwin=false
darwin=false
os400=false
hpux=false
case "`uname`" in
CYGWIN*) cygwin=true;;
Darwin*) darwin=true;;
OS400*) os400=true;;
HP-UX*) hpux=true;;
esac

这一段首先初始化了一些变量,然后通过一个case语句判断当前服务器操作系统是否以下面这几个开头,并对这些变量进行赋值

正常在linux环境运行时,uname结果为linux

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"`

这一段首先把当前脚本赋值给PRG,然后通过一个while循环获取catalina.sh的绝对路径,非链接。循环条件是-h判断PRG对应的不是一个软连接。

在循环中,首先ls -ld列出当前catalina.sh定位的详细信息赋值给ls

link=`expr "$ls" : '.*-> \(.*\)$'`

这一句是判断ls的实际值是否符合正则,正则内容是.*->\(.*\)$,这个正则的含义是xxx指向多组xxx,内容是.*$是结尾。

这里使用\(\)作用是改变输出结果,expr输出匹配上的\(\)中的内容而非长度,结果赋值给link。这里参考Linux部分expr指令

http://www.chymfatfish.cn/archives/linux#expr---%E5%AE%9E%E6%97%B6%E8%AE%A1%E7%AE%97

再往下if语句又使用expr,匹配link,而/dev/null是一个linux的特殊文件,这个文件接收到的任何数据都会被丢弃。因此,null这个设备通常也被成为位桶(bit bucket)或黑洞。因此这里首先匹配link是否是/xxx的形式,即/开头后面随意,然后把expr的输出丢弃掉(否则会输出到linux中),如果符合,PRG就等于link,如果不符合,PRG是PRG拼上link的结果。dirname命令会把后面的路径,除路径相关的内容全部排除。

[ -z "$CATALINA_HOME" ] && CATALINA_HOME=`cd "$PRGDIR/.." >/dev/null; pwd`
[ -z "$CATALINA_BASE" ] && CATALINA_BASE="$CATALINA_HOME"

这里[]表示里面是一个布尔运算,运算内容是-z,判断CATALINA_HOME为空时返回TRUE,&&表示前一条命令执行成功,才执行后面一条,后面一个表达式结果赋值给CATALINA_HOME,首先cd到PRGDIR目录的上级目录,cd的结果就是当前目录,丢给垃圾桶,然后执行pwd,结果赋值给CATALINA_HOME

第二个命令类似,先检查BASE变量赋值情况,没赋值则把CATALINA_HOME赋值给它

if [ -z "$CATALINA_PID" ];then
   mkdir -p $CATALINA_BASE/temp
   CATALINA_PID=$CATALINA_BASE/temp/catalina.pid
fi

在if语句中首先判断CATALINA_PID为空,如果为空,使用mkdir命令基于CATALINA_BASE构建temp目录,这里加-p做强制校验,父目录不存在会自动构建,最后将拼接catalina.pid的结果赋值给CATALINA_PID

if [ -z "$CATALINA_OUT" ];then
   CATALINA_OUT=/opt/cloud/logs/tomcat/logs/catalina.out
fi

这里在指定CATALINA_OUT的路径了,逻辑一样,首先判断CATALINA_OUT没有赋值,就用下面的路径赋值给这个变量

if [ -r "$CATALINA_BASE/bin/setenv.sh" ]; then
  . "$CATALINA_BASE/bin/setenv.sh"
elif [ -r "$CATALINA_HOME/bin/setenv.sh" ]; then
  . "$CATALINA_HOME/bin/setenv.sh"
fi

这里-r判断文件存在且可读,是为了执行这两个脚本。算是一个可以自定义的扩展点。

LOGGING_MANAGER=-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager

用一个java环境变量赋值LOGGING_MANAGER变量

if [ -n "$CLASSPATH" ] ; then
  CLASSPATH="$CLASSPATH":
fi

-n判断CLASSPATH变量长度非0,赋值给CLASSPATH

if [ -d "$CATALINA_BASE/server" ] ; then
   CLASSPATH="$CLASSPATH""$CATALINA_BASE"/server/:"$CATALINA_BASE"/server/*
else
   CLASSPATH="$CLASSPATH""$CATALINA_HOME"/server/:"$CATALINA_HOME"/server/*
fi

-d判断后面的内容存在且是目录,处理CLASSPATH

后面开始分各种环境处理

# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin; then
  [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
  [ -n "$JRE_HOME" ] && JRE_HOME=`cygpath --unix "$JRE_HOME"`
  [ -n "$CATALINA_HOME" ] && CATALINA_HOME=`cygpath --unix "$CATALINA_HOME"`
  [ -n "$CATALINA_BASE" ] && CATALINA_BASE=`cygpath --unix "$CATALINA_BASE"`
  [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi

这里是在调整格式,确保所有的路径名都符合UNIX格式

# 在CLASSPATH环境变量后面补上一些JAR文件
if [ ! -z "$CLASSPATH" ] ; then
  CLASSPATH="$CLASSPATH":
fi
CLASSPATH="$CLASSPATH""$CATALINA_HOME"/bin/bootstrap.jar

if [ -z "$CATALINA_OUT" ] ; then
  CATALINA_OUT="$CATALINA_BASE"/logs/catalina.out
fi

# 指定CATALINA_TMPDIR变量
# 如果CATALINA_TMPDIR没有值,就让CATALINA_TMPDIR=$CATALINA_BASE/temp
if [ -z "$CATALINA_TMPDIR" ] ; then
  # Define the java.io.tmpdir to use for Catalina
  CATALINA_TMPDIR="$CATALINA_BASE"/temp
fi

添加了几个额外路径:bootstrap.jar、catalina.out、temp目录

  • -n <string> 判断字符串是否长度非0;-z <string> 判断字符串是否长度为0。它们可以用来判断是否某个环境变量已经被设置

# 在执行java程序前把路径名转换成WINDOWS格式
if $cygwin; then
  JAVA_HOME=`cygpath --absolute --windows "$JAVA_HOME"`
  JRE_HOME=`cygpath --absolute --windows "$JRE_HOME"`
  CATALINA_HOME=`cygpath --absolute --windows "$CATALINA_HOME"`
  CATALINA_BASE=`cygpath --absolute --windows "$CATALINA_BASE"`
  CATALINA_TMPDIR=`cygpath --absolute --windows "$CATALINA_TMPDIR"`
  CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
  [ -n "$JAVA_ENDORSED_DIRS" ] && JAVA_ENDORSED_DIRS=`cygpath --path --windows "$JAVA_ENDORSED_DIRS"`
fi

下面该执行jar包了:

if [ $have_tty -eq 1 ]; then
  echo "Using CATALINA_BASE:   $CATALINA_BASE"
  echo "Using CATALINA_HOME:   $CATALINA_HOME"
  echo "Using CATALINA_TMPDIR: $CATALINA_TMPDIR"
  if [ "$1" = "debug" ] ; then
    echo "Using JAVA_HOME:       $JAVA_HOME"
  else
    echo "Using JRE_HOME:        $JRE_HOME"
  fi
  echo "Using CLASSPATH:       $CLASSPATH"
  if [ ! -z "$CATALINA_PID" ]; then
    echo "Using CATALINA_PID:    $CATALINA_PID"
  fi
fi

执行前首先做一些echo动作

if [ "$1" = "jpda" ] ; then
  ……
fi

如果执行catalina.sh的参数是jpda,具体执行内容先不看


if [ "$1" = "debug" ] ; then
  ……
  else
    shift
    if [ "$1" = "-security" ] ; then
      ……
    fi
  fi

如果是debug和security,也先不看

然后是参数为run的场景:

elif [ "$1" = "run" ]; then

  # 把参数run去掉
  shift
  
  # 如果参数是run -security,则启动Security Manager
  if [ "$1" = "-security" ] ; then
    echo "Using Security Manager"
    shift
    exec "$_RUNJAVA" $JAVA_OPTS $CATALINA_OPTS \
      -Djava.endorsed.dirs="$JAVA_ENDORSED_DIRS" -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
      

  # 如果参数是孤单的run,则在本窗口中启动tomcat服务器
  # 在本窗口中启动的方法是使用exec,让当前进程fork一个新进程来启动tomcat,当前进程是tomcat的父进程
  # 启动tomcat的类是org.apache.catalina.startup.Bootstrap.main("start");
  else
    exec "$_RUNJAVA" $JAVA_OPTS $CATALINA_OPTS \
      -Djava.endorsed.dirs="$JAVA_ENDORSED_DIRS" -classpath "$CLASSPATH" \
      -Dcatalina.base="$CATALINA_BASE" \
      -Dcatalina.home="$CATALINA_HOME" \
      -Djava.io.tmpdir="$CATALINA_TMPDIR" \
      org.apache.catalina.startup.Bootstrap "$@" start
  fi

如果参数是run,则在当前窗口启动tomcat服务器,这里可以看BootStrap#main点我跳转

然后是参数为start的场景,跟run场景类似,都是启动BootStrap#main方法

elif [ "$1" = "start" ] ; then

  # 把参数start去掉
  shift
  
  # 创建一个文件(如果文件不存在的话)$CATALINA_BASE/logs/catalina.out  
  touch "$CATALINA_BASE"/logs/catalina.out

  # 如果参数是start -security,则启动Security Manager
  if [ "$1" = "-security" ] ; then
    echo "Using Security Manager"
    shift
    "$_RUNJAVA" $JAVA_OPTS $CATALINA_OPTS \
      -Djava.endorsed.dirs="$JAVA_ENDORSED_DIRS" -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_BASE"/logs/catalina.out 2>&1 &

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

  # 如果参数是孤单的start,那么在新窗口中启动tomcat
  else
    "$_RUNJAVA" $JAVA_OPTS $CATALINA_OPTS \
      -Djava.endorsed.dirs="$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_BASE"/logs/catalina.out 2>&1 &

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

最后是stop场景,比较简单:

elif [ "$1" = "stop" ] ; then

  # 把参数stop去掉
  shift
  

  # 关闭tomcat服务器的类是org.apache.catalina.startup.Bootstrap->main("stop");
  # 注意:java -D 是设置系统属性
  exec "$_RUNJAVA" $JAVA_OPTS $CATALINA_OPTS \
    -Djava.endorsed.dirs="$JAVA_ENDORSED_DIRS" -classpath "$CLASSPATH" \
    -Dcatalina.base="$CATALINA_BASE" \
    -Dcatalina.home="$CATALINA_HOME" \
    -Djava.io.tmpdir="$CATALINA_TMPDIR" \
    org.apache.catalina.startup.Bootstrap "$@" stop

执行Bootstrap#stop方法即可

到这里就可以知道,tomcat通过catalina.sh启动,然后调用Bootstrap.jar开始执行tomcat容器加载,加载完成后,进而调用到java程序中的spring和servlet加载

脚本再后面的可以先不看了

Tomcat架构

Tomcat设计架构和组件

Tomcat启动初始化一系列组件,包括Server、Service、Connector、Container(Engine、Host、Context、Wrapper)、Executor等

Engine表示整个Tomcat服务器,Host表示一个虚拟主机,Context表示一个Web应用程序上下文

这四大类组件都是实现Lifecycle接口,因此具备init、start、stop、destroy生命周期方法

四大组件的整体架构如下图:

  • Server:一个Tomcat就是一个Server,即一个服务器,一个服务器会占据一个端口。若不同的域名解析的ip相同意味着机器是一致的,同时请求端口是一致的,意味着这些请求将被发到同一个server中

  • Service:Service是Connector与Container的封装,即服务器实例,一个Server中可以启动多个Service,即多个服务部署,但在目前分布式架构多集群部署的情况下,一般一个Tomcat就起一个Service了

  • Connector:每个Service有多个Connector,承担不同协议的通讯能力,例如负责HTTP的、负责HTTPS的等等,对外端口暴露其实是Connector决定的,即一个Server中有多个Connector,可能暴露8080、8081等等,请求这个ip:port的所有请求就会发往这个Server

  • Container:每个Service起一个Container,即一个容器/运行环境

  • Engine:每个Container有一个Engine,即处理引擎,负责将请求匹配到对应的Host上

  • Host:每个Container有多个Host,即虚拟主机/域名,根据请求对应的域名选择对应的Host处理。若域名不同解析出相同ip,且有相同端口,被发到一个server中后,由Engine负责解析将这些不同域名发给不同Host处理

  • Context:应用程序,即写代码启动的java程序,一个Container可以有多个Context,是Servlet的载体

  • Wrapper:Servlet包装,一个Wrapper包装一类Servlet,是为了应对每个请求构造单独Servlet实例的情况(并发场景单例servlet线程不安全),同时负责管理Servlet的全生命周期——构造、服务、销毁……

  • Servlet:应用程序中一个实现了service方法等请求处理全生命周期方法的功能模块,能对应解析传过来的请求中的GET、POST等方法,做对应处理

请求处理链路

一个请求经过tomcat转发到java服务的流程如下:

  1. 首先请求被转发到节点上,基于请求端口分配到不同的tomcat server,每个tomcat server实际上就是一个后端服务容器,一个server通过多个connector暴露多个端口,处理不同协议,例如服务1的HTTP协议绑定了80端口,服务2的HTTP协议绑定81端口,请求80,则被正确转发到对应的server中

  2. server拿到请求,继续向下找host,根据域名不同找对应的host,即一个服务可以提供不同域名的服务

  3. 路由到对应的host后,host管理一个或多个context,即servlet的载体,每个servlet处理的是请求的不同路径,根据路径差异转发到对应的context,由其中的servlet负责处理请求

处理链路如图所示:

Tomcat源码分析

源码安装

可以基于apache网址https://archive.apache.org/dist/tomcat/下载tomcat源码

下载后需要按照流程完成pom导入、Maven项目创建、JDK版本支持、修复jsp等工作,可以直接在网上搜索相关流程。

jsp修复可以修改org.apache.catalina.startup.ContextConfig中configureStart()方法

webConfig();
//添加JSP解析器初始化
context.addServletContainerInitializer(new JasperInitializer(), null);

完成后通过BootStrap.java启动

Tomcat的启动流程

从图中可以看出tomcat启动过程是跟它的几个核心组件息息相关的,由Server开始一层一层启动。

  • BootStrap启动整个tomcat,执行catalina的load和start方法

  • Catalina启动StandardServer

  • StandardServer在initInternal方法中调用StandardService的init方法,在startInternal中调用StandardService的start方法

  • StandService在initInternal中做了engine、Executor、Mapper、Connector、rotoclHandler的初始化,在startInternal中执行了上述各个组件的start

  • StandardEngine的startInternal方法执行了StandardHost的init和start,这里需要注意的是跟前面不一样,host的init方法不是engine的init中做的

  • StandardHost的startInternal方法触发StandardContext的init和start, 同时通过事件广播触发HostConfig中的app部署流程

  • StandardContext的startInternal方法触发StandardWrapper的init和start,同时完成web.xml的加载通过事件广播触发ServletContextListener的contextInitialized方法,同时调用StandardWrapper的loadServlet方法加载servlet

  • StandardWrapper加载servlet

LifeCycle - 顶级接口

可以看出来这些组件都实现Lifecycle接口,提供init、start、stop、destroy四个生命周期方法

在代码包里面,有LifeCycle接口四个方法的生命周期里程:

             start()
   -----------------------------
   |                           |
   | init()                    |
  NEW -»-- INITIALIZING        |
  | |           |              |     ------------------«-----------------------
  | |           |auto          |     |                                        |
  | |          \|/    start() \|/   \|/     auto          auto         stop() |
  | |      INITIALIZED --»-- STARTING_PREP --»- STARTING --»- STARTED --»---  |
  | |         |                                                            |  |
  | |destroy()|                                                            |  |
  | --»-----«--    ------------------------«--------------------------------  ^
  |     |          |                                                          |
  |     |         \|/          auto                 auto              start() |
  |     |     STOPPING_PREP ----»---- STOPPING ------»----- STOPPED -----»-----
  |    \|/                               ^                     |  ^
  |     |               stop()           |                     |  |
  |     |       --------------------------                     |  |
  |     |       |                                              |  |
  |     |       |    destroy()                       destroy() |  |
  |     |    FAILED ----»------ DESTROYING ---«-----------------  |
  |     |                        ^     |                          |
  |     |     destroy()          |     |auto                      |
  |     --------»-----------------    \|/                         |
  |                                 DESTROYED                     |
  |                                                               |
  |                            stop()                             |
  ----»-----------------------------»------------------------------

LifeCycleBase/LifeCycleMBeanBase - 生命周期管理模板方法

LifeCycleBase实现了一套init、start、stop、destroy方法的默认实现,同时提供initInternal、startInternal、stopInternal、destroyInternal四个空实现给子类使用

LifeCycleMBeanBase是Tomcat组件的通用模板父类,实现了initInternal、startInternal、stopInternal、destroyInternal

LifeCycleBase#init

public final synchronized void init() throws LifecycleException {
    ……
    try {
        setStateInternal(LifecycleState.INITIALIZING, null, false);
        initInternal();
        setStateInternal(LifecycleState.INITIALIZED, null, false);
    } catch (Throwable t) {
        handleSubClassException(t, "lifecycleBase.initFail", toString());
    }
}

除去一些状态变更,剩下的主要就是调用initInternal方法

LifeCycleBase#start

public final synchronized void start() throws LifecycleException {
        // 日志处理,略
        ……
        if (state.equals(LifecycleState.NEW)) {
            init();
        } else if (state.equals(LifecycleState.FAILED)) {
            stop();
        } else if (!state.equals(LifecycleState.INITIALIZED) && !state.equals(LifecycleState.STOPPED)) {
            invalidTransition(BEFORE_START_EVENT);
        }

        try {
            setStateInternal(LifecycleState.STARTING_PREP, null, false);
            startInternal();
            ……
    }

首先做state的判断和设置,如果之前发现是没初始化过的NEW状态,重新做初始化

然后核心是在setStateInternal

LifeCycleMBeanBase#initInternal

模板方法提供了bean注册能力,这个类就有点类似spring的IOC,也是先做bean的注册和加载,是基于JMX框架实现的

protected void initInternal() throws LifecycleException {
    if (oname == null) {
        oname = register(this, getObjectNameKeyProperties());
    }
}

可以看到注册流程在register中,继续跟进

// Note: This method should only be used once initInternal() has been called and before
protected final ObjectName register(Object obj, String objectNameKeyProperties) {
    ……
    try {
        on = new ObjectName(name.toString());
        Registry.getRegistry(null, null).registerComponent(obj, on, null);
    } catch ……
    return on;
}

但需要注意该方法的说明:

希望执行其他初始化的子类应重写此方法,确保 super.initInternal() 是重写方法中的第一个调用。

即它的子类的initInternal方法要首先通过LifeCycleMBeanBase#initInternal方法调用父类里面的注册方法。这一点可以在StandardServer中得到印证

setStateInternal

实际上核心就是通过此方法完成Event发送,完成一个观察者模式的监听逻辑

private synchronized void setStateInternal(LifecycleState state, Object data, boolean check)
        throws LifecycleException {
    ……
    this.state = state;
    String lifecycleEvent = state.getLifecycleEvent();
    if (lifecycleEvent != null) {
        fireLifecycleEvent(lifecycleEvent, data);
    }
}

fireLifecycleEvent

protected void fireLifecycleEvent(String type, Object data) {
    LifecycleEvent event = new LifecycleEvent(this, type, data);
    for (LifecycleListener listener : lifecycleListeners) {
        listener.lifecycleEvent(event);
    }
}

通过观察者模式发布事件和处理事件,每一个实现类都可以预先加载自己的观察者listener,实现lifecycleEvent方法,判断对应的event是否是自己所需要的

BootStrap

启动类,一般是服务中唯一的main方法所在,由catalina.sh启动

参考catalina.sh

main方法

可以看到catalina.sh,启动main方法的地方,将start作为参数传进去

org.apache.catalina.startup.Bootstrap "$@" start

接到指令,先执行BootStrap初始化

Bootstrap bootstrap = new Bootstrap();
try {
    bootstrap.init();

暂不细看,直接看BootStrap接收到命令的时候,如果是start命令,调用这里就分别调用了BootStrap的load方法和start方法

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 ......

分别看下load和start方法

load

private void load(String[] arguments) throws Exception {
    // Call the load() method
    String methodName = "load";
    ……
    method.invoke(catalinaDaemon, param);
}

这里的catalinaDaemon是什么?其实要去init方法中找线索

public void init() throws Exception {
    ……
    Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.getConstructor().newInstance();
    ……
    catalinaDaemon = startupInstance;
}

原来是Catalina实例,可知这里通过反射调用Catalina#load方法(点我跳转),按顺序加载server、service、engine、connector等,同时指导protocol加载

start

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

与load方法大同小异,也是通过反射到Catalina#start方法(点我跳转

Catalina

web应用程序全生命周期管理容器

load

parseServerXml(true);

通过digester技术解析web应用中配置的server.xml,参考parseServerXml方法生成从而生成Server组件,构造出来默认是StandardServer对象

getServer().setCatalina(this);
getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());

完成了一些Server中属性的配置

getServer().init();

调了StandServer#init方法,而StandardServer没有重写该方法,因此直接调用到LifeCycleBase#init点我跳转)的模板方法中,然后由模板方法调用到StandardServer#initInternal点我跳转

start

getServer().start();

跟load流程类似,也是一步步向下激活,首先是StandardServer#start方法,这里也是没有重写,直接调到LifeCycleBase#start点我跳转)中,然后由模板方法调用到StandardServer#startInternal点我跳转

parseServerXml

ConfigFileLoader
        .setSource(new CatalinaBaseConfigurationSource(Bootstrap.getCatalinaBaseFile(), getConfigFile()));
File file = configFile();

可以通过getConfigFile() 得知,加载的是public static final String SERVER_XML = "conf/server.xml"; ,即server.xml

InputStream inputStream = resource.getInputStream();
InputSource inputSource = new InputSource(resource.getURI().toURL().toString());
inputSource.setByteStream(inputStream);
digester.push(this);
……
digester.parse(inputSource);

这里就到了加载的地方,跟进看下

// --- Digester#parse ---
public Object parse(InputSource input) throws IOException, SAXException {
    configure();
    getXMLReader().parse(input);
    return root;
}

apache.Digester底层就是SAX,和spring加载xml底层是一套逻辑,不细看了

StandardServer

Server组件的实现

initInternal

super.initInternal();

首先调用LifeCycleMBeanBase#initInternal点我跳转)方法注册

for (Service service : services) {
    service.init();
}

下一个核心在services的变量这里。

StandardServer构造出来的时候services属性里面也构造好了,塞了StandardService,这里又调用到StandardService#init,忽略父类流程,直接看StandardService#initInternal点我跳转)方法

startInternal

for (Service service : services) {
    service.start();
}

主要做service的启动,参考StandardService#startInternal点我跳转)方法

StandardService

Service组件的实现

initInternal

init方法也是基于父类LifeCycleMBeanBase,注册完成后调用initInternal方法

engine.init();
executor.init();
mapperListener.init();
connector.init();

构造的StandardService中也构造了StandardEngine、connector等,完成初始化,参考StandardEngine#initInternal点我跳转)方法、Connector#initInternal点我跳转)方法

但这里有个坑:StandardEngine的初始化不会初始化后面的Host、Context,这两兄弟的初始化反而是在StandardEngine#startInternal点我跳转流程中

startInternal

engine.start();
executor.start();
connector.start();

做engine、executor、connector的启动流程,重点关注engine的启动,参考StandardEngine#startInternal点我跳转)方法

Connector

initInternal

protocolHandler.init();

主要是完成protocolHandler的初始化,在server.xml中配置的,协议,一般是Http11NioProtocol

Http11NioProtocol

init

主体方法继承自AbstractProtocol,负责协议初始化,是NioEndpoint的实现

initServerSocket();
initialiseSsl();

调用到NioEndpoint#bind方法负责创建Socket和SSL,到这里除了懒加载的组件都已经load完成了

StandardEngine

Engine组件实现

实现LifeCycleBase接口,同时继承containerBase,作为抽象容器的子类

initInternal

protected void initInternal() throws LifecycleException {
    getRealm();
    super.initInternal();
}

需要注意的是它实际上没有继续往下初始化子组件Host、Context

startInternal

protected void startInternal() throws LifecycleException {
    if (log.isInfoEnabled()) {
        log.info(sm.getString("standardEngine.start", ServerInfo.getServerInfo()));
    }
    // Standard container startup
    super.startInternal();
}

没有很多自己的代码,主要到了父类ContainerBase#startInternal方法

既然说下游的init不是在Engine#init完成,而是在Engine#start完成,那么顺着往下继续找找

继续跟进看下

Cluster cluster = getClusterInternal();
if (cluster instanceof Lifecycle) {
    ((Lifecycle) cluster).start();
}
Realm realm = getRealmInternal();
if (realm instanceof Lifecycle) {
    ((Lifecycle) realm).start();
}

首先执行cluster、realm的start方法

Container[] children = findChildren();

当Engine执行这个方法,找到的子类就是StandardHost

for (Container child : children) {
    results.add(startStopExecutor.submit(new StartChild(child)));
}

这里是做的异步启动,可以看出来任务的call方法就是子类的start方法,跟进StartChild类看看:

private static class StartChild implements Callable<Void> {
    private Container child;
    ……
    @Override
    public Void call() throws LifecycleException {
        child.start();
        return null;
    }
}

这里并不复杂,直接调用Container#start方法

这个Container是什么呢?它的中文名称是容器,还记得tomcat的层次:server-service-engine-host-context-wrapper,前两层server-service是服务级别,到了engine就是容器级别了

public abstract class ContainerBase extends LifecycleMBeanBase implements Container

抽象类ContainerBase是Container的实现类,同时继承了那个公共父类LifecycleMBeanBase

public class StandardHost extends ContainerBase implements Host

但是一路找来,都没看到StandarHost#init ,可以推测,StandarHost#init 可能是在StandardHost#start中执行的

而StandardHost就是ContainerBase的实现类,因此这里直接参考StandardHost#start点我跳转

StandardHost

start

继承了父类LifeCycleBase#start方法,与其他组件相比,虽然实现LifecycleMBeanBase,但是StandardHost#start方法显然丰富的多

public final synchronized void start() throws LifecycleException {
    ……
    if (state.equals(LifecycleState.NEW)) {
        init();
    } else if (state.equals(LifecycleState.FAILED)) {
        stop();
    } else if (!state.equals(LifecycleState.INITIALIZED) && !state.equals(LifecycleState.STOPPED)) {
        invalidTransition(BEFORE_START_EVENT);
    }
    try {
        setStateInternal(LifecycleState.STARTING_PREP, null, false);
        startInternal();
        ……
    } catch ……
}

可以看到,init、stop是根据不同指令执行的,startInternal则是自己的启动方法

可见StandardHost没有在init流程初始化,而是在start流程初始化的,而且是异步完成的

继续跟进startInternal方法

startInternal

protected void startInternal() throws LifecycleException {
    // Set error report valve
    String errorValve = getErrorReportValveClass();
    if ((errorValve != null) && (!errorValve.equals(""))) {
        ……
    }
    super.startInternal();
}

前面一大段都是些错误处理,核心反而也在父类ContainerBase#startInternal方法,这里前面StandardEngine实际上已经看过了,就是下面这一段流程:

Container[] children = findChildren();
List<Future<Void>> results = new ArrayList<>(children.length);
for (Container child : children) {
    results.add(startStopExecutor.submit(new StartChild(child)));
}

即完成寻找子组件,触发子组件的流程

StandardHost的子容器是StandardContext,因此这里直接到了StandardContext#start点我跳转)方法,这里如果直接用Tomcat的示例起服务,是没有Context的,可以基于Spring-boot的启动,可以看到这里启动的是Spring重写的TomcatEmbeddedContext

除了与StandardEngine类似的流程外,StandardHost还需要关注父类startInternal的一点在于:

setState(LifecycleState.STARTING);

这里设置STARTING状态,具体是干什么呢?

protected synchronized void setState(LifecycleState state) throws LifecycleException {
    setStateInternal(state, null, true);
}

调用到LifeCycleBase#setStateInternal方法,还记得在前面LifeCycleBase#setStateInternal点我跳转)方法看过,是发事件的方法,这里发的是Host的STARTING事件

而StandardHost的观察者是HostConfig,因此这里异步调用了HostConfig#lifecycleEvent点我跳转)方法

HostConfig

StandardHost的观察者

lifecycleEvent

if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
    check();
} else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
    beforeStart();
} else if (event.getType().equals(Lifecycle.START_EVENT)) {
    start();
} else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
    stop();
}

可以看到这里有对事件的分类路由,重点看下start方法

start

重点看下start方法

if (host.getDeployOnStartup()) {
    deployApps();
}

这里开始部署app了

deployApps

// Deploy XML descriptors from configBase
deployDescriptors(configBase, configBase.list());
// Deploy WARs
deployWARs(appBase, filteredAppPaths);
// Deploy expanded folders
deployDirectories(appBase, filteredAppPaths);

可以看到几种部署逻辑,包括war包部署,这里就把业务包部署起来了

StandardContext

startInternal

从start方法进来,路线和前面StandardHost完全一致,调用到父类LifeCycleBase#start点我跳转)方法,然后进入StandardContext#startInternal,里面流程比较多,可以关注几个点:

1. 将Loard的classLoader绑定到线程中,这样Digester在去classLoader时就可以取到Loard的classLoader

ClassLoader oldCCL = bindThread();

2. 触发CONFIGURE_START_EVENT事件,与StandardHost类似,Context的默认监听者是ContextConfig,这里到ContextConfig#lifecycleEvent点我跳转)方法,其中有web.xml的解析流程,非常非常核心

// Notify our interested LifecycleListeners
fireLifecycleEvent(CONFIGURE_START_EVENT, null);

3. 这个步骤触发context的子类加载,即StandardWrapper#initStandardWrapper#start点我跳转

// Start our child containers, if not already started
for (Container child : findChildren()) {
    if (!child.getState().isAvailable()) {
        child.start();
    }
}

4. 这里有一个核心步骤,唤醒所有的ServletContextListener执行业务方自行实现的contextInitialized方法,例如spring-MVC架构中的ContextLoaderListener就是在这里唤醒的,而想要配置自行实现Listener,可以在web.xml中设置<listener>标签

http://www.chymfatfish.cn/archives/spring-mvc#contextloaderlistener---%E5%85%A8%E5%B1%80%E4%B8%8A%E4%B8%8B%E6%96%87%E5%88%9D%E5%A7%8B%E5%8C%96
// Configure and call application event listeners
if (ok) {
    if (!listenerStart()) {
        log.error(sm.getString("standardContext.listenerFail"));
        ok = false;
    }
}

这里可以深入看下listenerStart方法

5. 到这里已经完成了StandardWrapper的初始化和start,开始加载servlet,调用到StandWrapper#load点我跳转)方法

// Load and initialize all "load on startup" servlets
if (ok) {
    if (!loadOnStartup(findChildren())) {
        log.error(sm.getString("standardContext.servletFail"));
        ok = false;
    }
}

这里可以深入看下loadOnStartup方法

listenerStart

String listeners[] = findApplicationListeners();
Object results[] = new Object[listeners.length];
……
// Sort listeners in two arrays
List<Object> eventListeners = new ArrayList<>();
List<Object> lifecycleListeners = new ArrayList<>();
for (Object result : results) {
    if ((result instanceof ServletContextAttributeListener) ||
            (result instanceof ServletRequestAttributeListener) || (result instanceof ServletRequestListener) ||
            (result instanceof HttpSessionIdListener) || (result instanceof HttpSessionAttributeListener)) {
        eventListeners.add(result);
    }
    if ((result instanceof ServletContextListener) || (result instanceof HttpSessionListener)) {
        lifecycleListeners.add(result);
    }
}
eventListeners.addAll(Arrays.asList(getApplicationEventListeners()));
setApplicationEventListeners(eventListeners.toArray());

可以看到这里取到了所有都的ApplicationListeners,一共有两类,分别是eventListeners和lifecycleListeners

其中lifecycleListeners就是之前看过的ServletContextListener实现类,例如例如spring-MVC架构中的ContextLoaderListener

注意,这里有个javax.ServletContextListener和jakarta.ServletContextListener的问题,是包迁移后的新老包的问题,其中jakarta包是新包,实际上就是一个

http://www.chymfatfish.cn/archives/spring-mvc#contextloaderlistener---%E5%85%A8%E5%B1%80%E4%B8%8A%E4%B8%8B%E6%96%87%E5%88%9D%E5%A7%8B%E5%8C%96

到这里已经把ServletContextListener的实现类全部都找到并封装好了,接下来发事件

ServletContextEvent event = new ServletContextEvent(getServletContext());
ServletContextEvent tldEvent = null;
……
for (Object instance : instances) {
    if (!(instance instanceof ServletContextListener)) {
        continue;
    }
    ServletContextListener listener = (ServletContextListener) instance;
    try {
        fireContainerEvent("beforeContextInitialized", listener);
        if (noPluggabilityListeners.contains(listener)) {
            listener.contextInitialized(tldEvent);
        } else {
            listener.contextInitialized(event);
        }
        fireContainerEvent("afterContextInitialized", listener);
    } catch (Throwable t) {

可以看到,这里调用了ServletContextListener#contextInitialized,而且还是同步执行的

可以参考ContextLoaderListener#contextInitialized方法

http://www.chymfatfish.cn/archives/spring-mvc#contextloaderlistener%E7%9A%84%E5%89%8D%E4%B8%96%E4%BB%8A%E7%94%9F

loadOnStartup

public boolean loadOnStartup(Container children[]) {
    // Collect "load on startup" servlets that need to be initialized
    // 找StandardWrapper
    TreeMap<Integer,ArrayList<Wrapper>> map = new TreeMap<>();
    for (Container child : children) {
        Wrapper wrapper = (Wrapper) child;
        int loadOnStartup = wrapper.getLoadOnStartup();
        if (loadOnStartup < 0) {
            continue;
        }
        Integer key = Integer.valueOf(loadOnStartup);
        map.computeIfAbsent(key, k -> new ArrayList<>()).add(wrapper);
    }
    // Load the collected "load on startup" servlets
    // 执行StandardWrapper#load方法
    for (ArrayList<Wrapper> list : map.values()) {
        for (Wrapper wrapper : list) {
            try {
                wrapper.load();
    ……

这里可以看到核心,先找到之前加载好的StandardWrapper,然后调用StandardWrapper#load点我跳转)方法

ContextConfig

StandardContext的观察者,收到Event,开始web.xml解析,非常核心

lifecycleEvent

if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
    configureStart();

也是做状态判断和路由,这里重点关注configureStart,其中就一个核心方法:webConfig();

protected synchronized void configureStart() {
    ……
    webConfig();

同时是一个同步锁方法,即web.xml只加载一次即可

webConfig

核心功能包括以下:

1、扫描/META-INF/lib/目录下的jar文件,如果在META-INF下含有web-fragment.xml文件,解析它;

2、确定确定这些xml片段的顺序

3、处理ServletContainerInitializers的实现类

4、将应用中的web.xml与orderedFragments进行合并,合并在WebXml类的merge方法中实现

5、将应用中的web.xml与全局的web.xml文件(conf/web.xmlweb.xml.default)进行合并

6、用合并好的WebXml来配置Context,这一步在处理servlet时,会为每个servlet创建一个wrapper,并调用addChild将每个wrapper作为context子容器,后续分析

protected void webConfig() {
   //xml解析器
   WebXmlParser webXmlParser = new WebXmlParser(context.getXmlNamespaceAware(),
            context.getXmlValidation(), context.getXmlBlockExternal());

   Set<WebXml> defaults = new HashSet<>();
   defaults.add(getDefaultWebXmlFragment(webXmlParser));
        
   //创建web.xml格式的对象,此时属性都为空
   WebXml webXml = createWebXml();        
        
   // 创建web.xml文件流
   InputSource contextWebXml = getContextWebXmlSource();
   //解析流,把内容封装到webXml
   if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
        ok = false;
   }
        
   //处理web-fragment.xml文件,分多个步骤
   //Step 1. 扫描/META-INF/lib/目录下的jar文件,如果在META-INF下含有web-fragment.xml文件,解析它
   Map<String,WebXml> fragments = processJarsForWebFragments(webXml, webXmlParser);

    // Step 2. 确定确定这些xml片段的顺序
    Set<WebXml> orderedFragments = null;
    orderedFragments = WebXml.orderWebFragments(webXml, fragments, sContext);

    // Step 3. 处理ServletContainerInitializers的实现类
    if (ok) {
        processServletContainerInitializers();
    }

    if  (!webXml.isMetadataComplete() || typeInitializerMap.size() > 0) {
        // Steps 4 & 5 都在processClasses里面,提前讲下
        //Steps 4 处理/WEB-INF/classes 下的注解类和 @HandlesTypes matches
        //Steps 5 处理 引入的JARs中的注解类和 @HandlesTypes matches
        processClasses(webXml, orderedFragments);
    }

    if (!webXml.isMetadataComplete()) {
        // Step 6. 将应用中的web.xml与orderedFragments进行合并,合并在WebXml::merge方法中
        if (ok) {
            ok = webXml.merge(orderedFragments);
        }

        // Step 7. 将应用中的web.xml与全局的web.xml文件(conf/web.xml和web.xml.default)合并
        // Have to merge defaults before JSP conversion since defaults
        // provide JSP servlet definition.
        webXml.merge(defaults);
            
        // Step 8. Convert explicitly mentioned jsps to servlets
        if (ok) {
            convertJsps(webXml);
        }

        // Step 9. 通过webXml对象,间接把web.xml构建成 Context对象
        if (ok) {
            configureContext(webXml);
        }
      
    }
    ……

最核心的几个步骤在里面都有体现了

StandardWrapper

前面已经了解过,Wrapper实际上就是Servlet的封装,以spring-MVC的DispatcherServlet为例,StandardWrapper实际上就是一个持有DispatcherServlet实例引用并且能够对其进行加载、调用等操作的箱子

init/start

都没有自己的实现,用的就父类里面的init、start、initInternal、startInternal

最核心的是从StandardContext#startInternal点我跳转)中第五步骤调用过来的load方法

load

public synchronized void load() throws ServletException {
    instance = loadServlet();
    if (!instanceInitialized) {
        initServlet(instance);
    }
    ……
}

核心是这里加载方法initServlet,跟进去看:

private synchronized void initServlet(Servlet servlet) throws ServletException {
    if (instanceInitialized) {
        return;
    }
    ……
            servlet.init(facade);
    ……
        instanceInitialized = true;

如果已经加载过了,则不再加载,否则执行Servlet#init 方法,可以参考spring-MVC部分:

http://www.chymfatfish.cn/archives/spring-mvc#init

到这里整个Tomcat容器、spring-MVC的大上下文环境,以及Servlet的小上下文环境都启动了,服务就起好了

0

评论区