简介
The Apache Tomcat® software is an open source implementation of the Java Servlet, JavaServer Pages, Java Expression Language and Java WebSocket technologies. The Java Servlet, JavaServer Pages, Java Expression Language and Java WebSocket specifications are developed under the Java Community Process.
The Apache Tomcat software is developed in an open and participatory environment and released under the Apache License version 2. The Apache Tomcat project is intended to be a collaboration of the best-of-breed developers from around the world. We invite you to participate in this open development project. To learn more about getting involved, click here.
Apache Tomcat software powers numerous large-scale, mission-critical web applications across a diverse range of industries and organizations. Some of these users and their stories are listed on the PoweredBy wiki page.
Apache Tomcat, Tomcat, Apache, the Apache feather, and the Apache Tomcat project logo are trademarks of the Apache Software Foundation.以上的简介自来 Apache Tomcat 官网,Tomcat服务器是一个免费的开放源代码的Web应用服务器。Tomcat是Apache软件基金会(Apache Software Foundation)的Jakarta项目中的一个核心项目,由Apache、Sun和其他一些公司及个人共同开发而成。由于有了Sun的参与和支持,最新的Servlet 和JSP规范总是能在Tomcat中得到体现,因为Tomcat技术先进、性能稳定,而且免费,因而深受Java爱好者的喜爱并得到了部分软件开发商的认可,是目前比较流行的Web应用服务器。
本文基于Tomcat9的源码进行解读,不同的版本实现之间可能会有一些差异,请注意区分。
启动脚本 startup.sh
Tomcat 支持不同的平台,其启动方式都大同小异,都提供了相应的启动脚本,windows 平台下是 startup.bat
,而在 linux 环境下则是 startup.sh
,接下来的介绍都将以 linux 平台为主进行介绍。如下是 linux 平台下的 startup.sh
节选:
1 | PRGDIR=`dirname "$PRG"` |
显然,startup.sh
只是去调用了catalina.sh
而已,所以我们再来看看catalina.sh
的内容(为什么叫 catalina,我特意去查了一下词典和相关的资料,我们知道一个著名的 web 服务器就叫 apache,apache 有一个中文名称是:武装直升机,牛吧。但是,请再看看 catalina 的另外一个中文名称:远程轰炸机,哈哈,是不是更牛!当然,也许 tomcat 的作者并不是取此意,但具体的原因网上已经查不到了。):
1 | set _EXECJAVA=%_RUNJAVA% |
Tomcat 是一个由 java 语言编写的开源服务器,从脚本中我们可以看主,其启动类是 Bootstrap ,而且指定了调用的方法为start()
,因此我们就从这个类开始深入,一步步来看看整个启动的过程。
Catalina.sh
的脚本超过 600 行,包括了平时我们在使用Tomcat时的各种命令实现(比如start, stop, run 等),其启动的主要流程如下(同时对于 CYGWIN 进行了支持):
设置各类环境变量
设置
classpath
解析各个配置属性
设置安全管理器
调用
org.apache.catalina.startup.Bootstrap.start
开始整个启动过程启动时的主要脚本如下:
1 | touch "$CATALINA_OUT" |
Tomcat 是一个由 java 语言编写的开源服务器,从脚本中我们可以看主,其启动类是 Bootstrap ,而且指定了调用的参数start
,因此我们就从这个类开始深入,一步步来看看整个启动的过程。
启动入口Bootstrap.main()
从上一节我们知道,Tomcat 的启动入口在 Bootstrap.main()
,我们进入这个方法看看(注意:省略了很多非关注点的代码):
1 | public static void main(String args[]) { |
Bootstrp.init() -> Catalina.init()
主要过程的代码还是比较简洁的,这利益于Tomcat采用的 Lifecycle 模式,将整个过程封装到组件的各个生命周期中去了。关于 Lifecycle 设计模式的讲解可以关注我的另外一篇文章Tomcat对于Lifecycle的精妙使用。从源码看,先是初始化了bootstrap.init
,之后将加载参数后进入到了bootstrap.start
过程,下面我们先看看bootstrap.init
的源码:
1 | /** |
可以看到,所做的一切就是初始化一个org.apache.catalina.startup.Catalina
实例,并设置其classLoader
,过程如下:
- 初始化路径
- 初始化类加载器:初始化Tomcat类加载器:commonLoader、catalinaLoader、sharedLoader
commonLoader无父加载器,catalinaLoader和sharedLoader的父加载器都是commonLoader,其中若tomcat的配置文件没有配置:server.loader
则catalinaLoader=commonLoader,同理,没配置shared.loader……,这三种都是URLClassLoader,使用Java 中的安全模型; - 初始化Boostrap的Catalina对象:通过反射生成Catalina对象,并通过反射调用
setParentClassLoader
方法设置其父 ClassLoader为sharedLoader。为什么要用反射,不直接在声明的时候生成对象?使用反射来生成实例的原因是因为在tomcat的发展历史中可能不止Catalina一种启动方式,现在看代码已经没必要了。 - 其他:主线程的classLoader设置为catalinaLoader,安全管理的ClassLoad设置为catalineLoader。
其start
方法同样简单,刚刚不是初始化了一个Catalina
实例吗,那么现在的start
方法就是被代理到Catalina#start()
方法上去了,源码如下:
1 | public void start() throws Exception { |
所以,整个启动过程其实是在Catalina#start()
中。
Bootstrap.start() -> Catalina.start()
接下来的初始化过程就交由到 Catalina.start
方法了,看看主要代码如下:
1 | /** |
这是整个启动过程的重头戏,我们将分段讲解其各个环节。
初始化 Server.load()
从源码中可以看到,当 server == null
时,将会执行 Server 的初始化工作,调用其 load()
方法设置 Server的各个参数(我们经常打交道的 server.xml
解析与设计工作在此完成)。我们看看这个方法的主要执行过程(省略非关注点代码):
1 | public void load() { |
整个过程如下:
- 初始化各类目录结构
- 初始化命名空间,为XML解析铺垫
- 初始化
Digester
,开始解析server.xml
并将解析后的内容设置到Server
对象中(默认的配置文件定义:protected String configFile = "conf/server.xml";
)。 - 获取
Server
并执行其init
方法。
关于
Digester
的用法可以去参考我另外的文章:Digester:将XML转换为Java对象
初始化 Server.init()
Server 同时实现了Lifecycle接口,其具体的init
过程我们可以从StandardServer.initInternal
方法进行研究:
1 | protected void startInternal() throws LifecycleException { |
除了设置状态,触发相应的事件之外,这里还启动了 JNDI 的生命周期,用于完成 JNDI 的配置和初始化过程,JNDI 是 J2EE 规范的组件,有兴趣的同学可以参考我的另外一篇文章:有趣的 JNDI。
紧接着就是循环的启动所有的Service
组件,注意,这里是Service
而不是Server
,Service
和Connector
,Engine
,Host
一样,是Tomcat的核心组件,后面会详细的讲解。
StandardServer
只是 Tomcat Server 的一个标准实现,相较于标准实现,它还有另外一个版本:EmbeddedTomcat
,即内嵌的Tomcat。
初始化 Service.init()
跟Server
一样,Service的
实现也叫StandardService
,其初始化过程也是StandardService.initInternal()
中:
1 | protected void initInternal() throws LifecycleException { |
过程包括如下的几个核心点:
初始化
Engine
,Engine
在整个Tomcat源码框架中的戏份不多,当用户的请求到来时,到MapperData
中获取对应的Host
,并让请求通过Host
的Pipeline
。初始化
Executor
,这是线程池的配置,在server.xml
的中配置如下所示:1
2
3
4
5<!--The connectors can use a shared executor, you can define one or more named thread pools-->
<!--
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="150" minSpareThreads="4"/>
-->初始化
MapperListener
,这一步非常的简单,很容易就被忽略,但其实 非常非常 的重要,如果想要弄清楚Tomcat是如何分发路由请求,一定要了解这个组件。我的另外一篇文章Tomcat的路由游戏对这个有详细的讲解。初始化各个
Connector
,这个组件非常重要,这个我们单独用一节来讲解。
初始化 Connector.init()
Connector
是整个Tomcat中比较重要的一个组件,甚至说是最核心的组件都不为过,它的主要任务是负责接收浏览器发过来的TCP连接请求,创建 Request 和 Response 对象来封装接收、发送数据,然后把数据交给Container进行处理。
如果你仔细查看过server.xml
的话,会发现一个Service
节点下可以有很多的Connector
,每个Connector
都可以监听不同的端口,采用不同的协议来和用户交互。如下所示(我们在8080端口监听普通的请求,在 8443 监听 SSL 请求):
1 | <Connector port="8080" protocol="HTTP/1.1" |
当然以上这些工作都是在它启动之后,它的初始化过程其实是比较简单的,代码同样也是在Connector.initInternal()
中,核心代码如下:
1 | protected void initInternal() throws LifecycleException { |
就是根据不同的Connector
配置进行必要的设置(如SSL
的支持等),之后就交给各个ProtocolHandler
去根据不同的协议进行相应的初始化,常见的如Http11NioProtocol
,AjpNioProtocol
等。
服务启动 Server.start()
随着Connector
的初始化完成,Server.load()
过程也结束,接下来就是Server.start()
过程。
分析过init()
方法后再来看其start()
就简单得多了,几乎是整个init()
过程的复制粘贴,只不过将过程中的init()
方法换成了start()
方法,start()
方法让各个组件正式启动开始服役,为用户提供服务,各个ProtocolHandler
开始监听网络端口,接收用户的请求并作出响应。
从这个过程中可以看到,Tomcat的整个容器构成是非常有层次的,分别是
Server
->Service
->Connector
/Engine
->Host
。我有另外一篇文章专门讲解了这些组件之间的关系:Tomcat源码解读:组件。
这里要提一下我们的Servlet
的注册入口,其实是隐藏在Context.start()
过程中的,在这个过程中发布了一个事件fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);
,而这个事件会被ContextConfig
捕获,如下所示:
1 | public void lifecycleEvent(LifecycleEvent event) { |
一路跟踪的路径如下:webConfig()
-> configureContext(webXml)
-> context.addServletMappingDecoded(urlPattern, jspServletName, true);
这就是我们在web.xml
中配置的所有的Servlet
注册过程。
优雅的关闭:ShutdownHook
回过头去看Catalina.start()
知道,在完成 Server.start()
后还有一段代码:
1 | // Register shutdown hook |
这是一段标准的Java钩子函数的注册过程,Runtime.getRuntime().addShutdownHook
注册的钩子将会在JVM退出时执行。
Tomcat在这里注册的钩子用于保证优雅的关闭Tomcat容器,包括关闭打开的JNDI资源,数据库驱动,关闭线程池,清理fork出来的子线程等等。部分代码如下:
1 | /** |
那么,Tomcat是如何知道容器中有哪些由于用户的请求而fork出来的线程,并在关闭时及时的清理呢?可以关注我的另外一篇文章:Tomcat源码解读:Connector之一个请求的完整历程。
总结
至此,整个Tomcat的启动过程结束,可以看出还是比较 简洁 的,结构非常的清晰,遵循Lifecycle
的生命周期,在init
和start
阶段就完整了整个容器的构建过程。
通过这个过程的分析我们可以学习到怎么构建一个层次分明的容器关系,怎么利用 Lifecycle
模式分而治之地编码,怎么优雅地关闭容器。同时也了解到Tomcat各个组件的关系及用途,能够更好地优化Tomcat的运行性能,进而定制更适合自己需要的Tomcat。