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 "%r" %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 "%r" %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-name
和url-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指令
再往下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服务的流程如下:
首先请求被转发到节点上,基于请求端口分配到不同的tomcat server,每个tomcat server实际上就是一个后端服务容器,一个server通过多个connector暴露多个端口,处理不同协议,例如服务1的HTTP协议绑定了80端口,服务2的HTTP协议绑定81端口,请求80,则被正确转发到对应的server中
server拿到请求,继续向下找host,根据域名不同找对应的host,即一个服务可以提供不同域名的服务
路由到对应的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启动
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#init
和StandardWrapper#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>标签
// 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包是新包,实际上就是一个
到这里已经把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
方法
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.xml
和web.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部分:
到这里整个Tomcat容器、spring-MVC的大上下文环境,以及Servlet的小上下文环境都启动了,服务就起好了
评论区