上一篇文章我们全面的介绍了Tomcat各文件目录的作用、源码各模块功能和组件之间的关系,下面我们从源码的角度去看看Tomcat是如何工作的。
tomcat为我们提供了启动和停止脚本,在bin目录下:startup.sh/startup.bat;shutdown.sh/shutdown.bat
下面我们以startup.sh启动脚本为例,看一下Tomcat如何运行的。
# ...省略部分逻辑代码
PRGDIR=`dirname "$PRG"`
EXECUTABLE=catalina.sh
exec "$PRGDIR"/"$EXECUTABLE" start "$@"
由于脚本中存在大量的环境变量获取的逻辑。我们只需要看关键代码即可,我们发现startup.sh最终是执行了catalina.sh,由于catalina.sh脚本定义了Tomcat服务器中所有的启动,运行,停止脚本,因此在这里我们只看一下start部分的逻辑即可,大量无关逻辑的先跳过。
# ...省略部分逻辑代码
eval $_NOHUP "\"$_RUNJAVA\"" "\"$CATALINA_LOGGING_CONFIG\""
$LOGGING_MANAGER "$JAVA_OPTS" "$CATALINA_OPTS" \
-D$ENDORSED_PROP="\"$JAVA_ENDORSED_DIRS\"" \
-classpath "\"$CLASSPATH\"" \
-Dcatalina.base="\"$CATALINA_BASE\"" \
-Dcatalina.home="\"$CATALINA_HOME\"" \
-Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
org.apache.catalina.startup.Bootstrap "$@" start \
>> "$CATALINA_OUT" 2>&1 "&"
至此我们可以看出来,脚本中设置了一些服务器路径作为环境变量,最终是通过java执行了org.apache.catalina.startup.Bootstrap"$@" start 指令,那我们直接看Bootstrap启动类即可;
2.1 初探Bootstrap启动类(入口类)
public static void main(String args[]) {
synchronized (daemonLock) {
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
//初始化 catalinaDaemon = Catalina
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
// When running as a service the call to stop will be on a new
// thread so make sure the correct class loader is used to
// prevent a range of class not found exceptions.
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
}
try {
String command = "start";
if (args.length > 0) {
command = args[args.length - 1];
}
if(){
//...省略部分代码
}else if (command.equals("start")) {
daemon.setAwait(true);
daemon.load(args);
daemon.start();
if (null == daemon.getServer()) {
System.exit(1);
}
}
// ...省略部分代码
}
启动的时候执行了3个方法
daemon.setAwait(true); //设置服务等待,直到接收到shutdown指令
daemon.load(args); //执行加载
daemon.start(); //执行开始
下面我们从源码角度分析Tomcat的启动流程(放大看)。
2.2 启动流程
核心组件说
组件名称 | 说明 |
---|---|
Server | 表示整个Servlet容器,Tomcat运行环境中只有唯一一个Server实例。 |
Service | Service可以包含多个Connector,这些Connector共享同一个Container来处理请求。在一个Tomcat实例内可以包含任意多个Service实例,它们彼此独立。 |
Connector | 链接器,用于监听并转化Socket请求,同时将读取的Socket请求交由Container处理,支持不同的协议以及不同的I/O实现方式。 |
Container | Container表示能够执行客户端请求并返回响应的一类对象。Tomcat中存在不同级别的容器:Engine,Host,Context,Wrapper。 |
Engine | Engine表示整个Servlet引擎。在Tomcat中Engine为最高层级的容器对象。尽管Engine不是直接处理请求的容器,却是获取目标容器的入口。 |
Host | Host作为一类容器,表示Servlet引擎(Engine)中的虚拟机,与一个服务器的网络名有关,如域名等。客户端可以使用这个网络名连接服务器,这个名称必须要在DNS服务器上注册。 |
Context | Context作为一类容器,用于表示ServletContext,在Servlet规范中,一个ServletContext即表示一个独立的Web应用。 |
Wrapper | Wrapper作为一类容器,用于表示Web应用中定义的Servlet。 |
Executor | 共享线程池 |
通过阅读源码,我们初步总结Tomcat通过Bootstrap启动类的Main方法为入口,逐级调用Tomcat各组件的init方法及start方法从而完成Tomcat服务器的运行。
2.3 核心配置文件 server.xml
日常配置最多的就是server.xml,仔细看各配置项,所有的组件基本上都包含在了该文件中了,Server,Service,Connector,Engine,Host等,那么Tomcat是如何通过配置文件与各组件之间建立联系的呢?
2.3.1 Xml解析组件Digester
Tomcat在Catalina初始化过程中使用Digester来解析server.xml。并创建出应用服务器。Digester通过流读取XML文件,当识别出特定的XML节点后便会执行特定的动作,或者创建Java对象,或者执行对象的某个方法。
如以下解析server.xml的代码(部分),声明Server类为StandardServer,执行setServer方法。
// Configure the actions we will be using
digester.addObjectCreate("Server",
"org.apache.catalina.core.StandardServer",
"className");
digester.addSetProperties("Server");
digester.addSetNext("Server",
"setServer",
"org.apache.catalina.Server");
简单一点来说其实就是相当于执行了以下方法
Server server=new StandardServer();
//... set server properties
setServer(server);
2.4 生命周期 Lifecycle
Tomcat中所有组件基本上都存在初始化、启动、停止、销毁等动作,以及相应的执行前、执行中、执行后状态。为此抽象出了一个Lifecycle通用接口。
每个生命周期方法可能对应数个状态的转换,生命周期状态由LifecycleBase抽象类自动为我们变更,并且发布各状态变更之后的处理事件通知相关监听处理类进行处理。
2.4.1 Container
Tomcat使用Container表示容器,Container可以添加并维护子容器,因此Engine、Host、Context、Wrapper均继承自Container。
此外,Tomcat的Container还有一个很重要的功能,就是后台处理,在很多情况下,Container需要执行一些异步处理,而且是定期执行,如每隔30秒执行一次,Tomcat对于Web应用文件变更的扫描就是通过该机制实现的。
Tomcat针对后台处理,在Container上定义了backgroundProcess()方法,基础抽象类(ContainerBase)确保在启动组件的同时,异步启动后台处理。
因此,在绝大多数情况下,各个容器组件仅需要实现Container的backgroundProcess()方法即可,不必考虑创建异步线程。
2.4.2 Pipeline和Valve
Tomcat采用责任链模式实现客户端的请求处理,它定义了Pipeline(管道)和Valve(阀门)两个接口,前者用于构造职责链,后者代表职责链上的每个处理器。
Pipeline中维护了一个基础的Valve,它始终位于Pipeline的末端(即最后执行),封装了具体的请求处理和输出响应的过程。
然后,通过addValve()方法,可以为Pipeline添加其它的Valve。后添加的Valve位于基础Valve之前,并按照添加顺序执行。Pipeline通过获得首个Valve来启动整个链条的执行。
比如:默认配置下,Host的实现是StandardHost,它的主要功能交给了StandardHostValve来做。额外配置一个日志的功能就特别方便了,只需要在管道上再加一个日志功能的阀门即可实现。
Tomcat主要就是通过解析server.xml配置文件,从而构建出各个相关组件,利用生命周期将各个组件之间串联起来,最终提供给我们来使用。
Tomcat作为经久不衰的一款轻量级Java应用服务器,它的设计有很多值得我们借鉴的地方。它的一些优秀的组件比如xml解析的工具Digester我们可以单独拿出来使用。它的思想比如各组件之间松耦合我们可以参考。
它的设计模式的实现比如生命周期的模板方法模式,管道阀门组合的责任链模式等也是值得我们去学习的东西。下一节我们将针对应用加载,去分析Tomcat是如何保证作为一个服务器去运行多个应用而相互之间又保持隔离的。
Redis系列:
Redis(一):单线程为何还能这么快
Redis(二):内存模型及回收算法
Redis(三):持久化
Redis(四):主从同步
ElasticSearch系列:
Elasticsearch(一):概述
Elasticsearch(二):核心
Elasticsearch(三):实战
RocketMQ系列:
RocketMQ—NameServer总结及核心源码剖析
RocketMQ—Producer(一)启动流程解密
Tomcat系列:
Tomcat项目结构及架构分析
关注IT巅峰技术,私信作者,获取以下2021全球架构师峰会PDF资料。