Tomcat 源码解读——启动篇

简介

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PRGDIR=`dirname "$PRG"`
EXECUTABLE=catalina.sh

# Check that target executable exists
if $os400; then
# -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 "$@"

​ 显然,startup.sh只是去调用了catalina.sh而已,所以我们再来看看catalina.sh的内容(为什么叫 catalina,我特意去查了一下词典和相关的资料,我们知道一个著名的 web 服务器就叫 apache,apache 有一个中文名称是:武装直升机,牛吧。但是,请再看看 catalina 的另外一个中文名称:远程轰炸机,哈哈,是不是更牛!当然,也许 tomcat 的作者并不是取此意,但具体的原因网上已经查不到了。):

1
2
3
4
5
6
set _EXECJAVA=%_RUNJAVA%
set MAINCLASS=org.apache.catalina.startup.Bootstrap
set ACTION=start
set SECURITY_POLICY_FILE=
set DEBUG_OPTS=
set JPDA=

​ Tomcat 是一个由 java 语言编写的开源服务器,从脚本中我们可以看主,其启动类是 Bootstrap ,而且指定了调用的方法为start(),因此我们就从这个类开始深入,一步步来看看整个启动的过程。

Catalina.sh的脚本超过 600 行,包括了平时我们在使用Tomcat时的各种命令实现(比如start, stop, run 等),其启动的主要流程如下(同时对于 CYGWIN 进行了支持):

  • 设置各类环境变量

  • 设置classpath

  • 解析各个配置属性

  • 设置安全管理器

  • 调用org.apache.catalina.startup.Bootstrap.start开始整个启动过程

    启动时的主要脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 \
-D$ENDORSED_PROP="\"$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_OUT" 2>&1 "&"

else
eval $_NOHUP "\"$_RUNJAVA\"" "\"$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 "&"

fi

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

echo "Tomcat started."

​ Tomcat 是一个由 java 语言编写的开源服务器,从脚本中我们可以看主,其启动类是 Bootstrap ,而且指定了调用的参数start,因此我们就从这个类开始深入,一步步来看看整个启动的过程。

启动入口Bootstrap.main()

​ 从上一节我们知道,Tomcat 的启动入口在 Bootstrap.main(),我们进入这个方法看看(注意:省略了很多非关注点的代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static void main(String args[]) {
synchronized (daemonLock) {
Bootstrap bootstrap = new Bootstrap();
try {
// 初始化 bootstrap
bootstrap.init();
} catch (Throwable t) {
// 省略非关注点代码 ...
}
daemon = bootstrap;
}

try {
if (command.equals("start")) {
daemon.setAwait(true);
daemon.load(args);

// 开始启动
daemon.start();
if (null == daemon.getServer()) {
System.exit(1);
}
}
} catch (Throwable t) {
// 省略非关注点代码 ...
}
}

Bootstrp.init() -> Catalina.init()

​ 主要过程的代码还是比较简洁的,这利益于Tomcat采用的 Lifecycle 模式,将整个过程封装到组件的各个生命周期中去了。关于 Lifecycle 设计模式的讲解可以关注我的另外一篇文章Tomcat对于Lifecycle的精妙使用。从源码看,先是初始化了bootstrap.init,之后将加载参数后进入到了bootstrap.start过程,下面我们先看看bootstrap.init的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* Initialize daemon.
*
* @throws Exception Fatal initialization error
*/
public void init() throws Exception {

initClassLoaders();

Thread.currentThread().setContextClassLoader(catalinaLoader);

SecurityClassLoad.securityClassLoad(catalinaLoader);

// Load our startup class and call its process() method
if (log.isDebugEnabled())
log.debug("Loading startup class");
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();

// Set the shared extensions class loader
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
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);

catalinaDaemon = startupInstance;

}

​ 可以看到,所做的一切就是初始化一个org.apache.catalina.startup.Catalina实例,并设置其classLoader,过程如下:

  1. 初始化路径
  2. 初始化类加载器:初始化Tomcat类加载器:commonLoader、catalinaLoader、sharedLoader
    commonLoader无父加载器,catalinaLoader和sharedLoader的父加载器都是commonLoader,其中若tomcat的配置文件没有配置:server.loader则catalinaLoader=commonLoader,同理,没配置shared.loader……,这三种都是URLClassLoader,使用Java 中的安全模型;
  3. 初始化Boostrap的Catalina对象:通过反射生成Catalina对象,并通过反射调用setParentClassLoader方法设置其父 ClassLoader为sharedLoader。为什么要用反射,不直接在声明的时候生成对象?使用反射来生成实例的原因是因为在tomcat的发展历史中可能不止Catalina一种启动方式,现在看代码已经没必要了。
  4. 其他:主线程的classLoader设置为catalinaLoader,安全管理的ClassLoad设置为catalineLoader。

​ 其start方法同样简单,刚刚不是初始化了一个Catalina实例吗,那么现在的start方法就是被代理到Catalina#start()方法上去了,源码如下:

1
2
3
4
5
6
7
public void start() throws Exception {
if (catalinaDaemon == null) init();

Method method = catalinaDaemon.getClass().getMethod("start", (Class[]) null);
method.invoke(catalinaDaemon, (Object[]) null);

}

​ 所以,整个启动过程其实是在Catalina#start()中。

Bootstrap.start() -> Catalina.start()

​ 接下来的初始化过程就交由到 Catalina.start 方法了,看看主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* Start a new server instance.
*/
public void start() {

if (getServer() == null) {
load();
}

if (getServer() == null) {
log.fatal("Cannot start server. Server instance is not configured.");
return;
}

long t1 = System.nanoTime();

// Start the new server
try {
getServer().start();
} catch (LifecycleException e) {
log.fatal(sm.getString("catalina.serverStartFail"), e);
try {
getServer().destroy();
} catch (LifecycleException e1) {
log.debug("destroy() failed for failed Server ", e1);
}
return;
}

long t2 = System.nanoTime();
if(log.isInfoEnabled()) {
log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");
}

// Register shutdown hook
if (useShutdownHook) {
if (shutdownHook == null) {
shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);

// If JULI is being used, disable JULI's shutdown hook since
// shutdown hooks run in parallel and log messages may be lost
// if JULI's hook completes before the CatalinaShutdownHook()
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager) logManager).setUseShutdownHook(
false);
}
}

if (await) {
await();
stop();
}
}

​ 这是整个启动过程的重头戏,我们将分段讲解其各个环节。

初始化 Server.load()

​ 从源码中可以看到,当 server == null时,将会执行 Server 的初始化工作,调用其 load() 方法设置 Server的各个参数(我们经常打交道的 server.xml 解析与设计工作在此完成)。我们看看这个方法的主要执行过程(省略非关注点代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void load() {
...
initDirs();
initNaming();
...
// Create and execute our Digester
Digester digester = createStartDigester();
...
InputSource inputSource = null;
InputStream inputStream = null;
File file = null;
...
file = configFile();
inputStream = new FileInputStream(file);
inputSource = new InputSource(file.toURI().toURL().toString());
...
inputSource.setByteStream(inputStream);
digester.push(this);
digester.parse(inputSource);
...
getServer().setCatalina(this);
...
getServer().init();
}

​ 整个过程如下:

  1. 初始化各类目录结构
  2. 初始化命名空间,为XML解析铺垫
  3. 初始化 Digester,开始解析server.xml并将解析后的内容设置到Server对象中(默认的配置文件定义:protected String configFile = "conf/server.xml";)。
  4. 获取Server并执行其init方法。

关于Digester的用法可以去参考我另外的文章:Digester:将XML转换为Java对象

初始化 Server.init()

​ Server 同时实现了Lifecycle接口,其具体的init过程我们可以从StandardServer.initInternal方法进行研究:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void startInternal() throws LifecycleException {

fireLifecycleEvent(CONFIGURE_START_EVENT, null);
setState(LifecycleState.STARTING);

globalNamingResources.start();

// Start our defined Services
synchronized (servicesLock) {
for (int i = 0; i < services.length; i++) {
services[i].start();
}
}
}

​ 除了设置状态,触发相应的事件之外,这里还启动了 JNDI 的生命周期,用于完成 JNDI 的配置和初始化过程,JNDI 是 J2EE 规范的组件,有兴趣的同学可以参考我的另外一篇文章:有趣的 JNDI

​ 紧接着就是循环的启动所有的Service组件,注意,这里是Service而不是ServerServiceConnectorEngineHost一样,是Tomcat的核心组件,后面会详细的讲解。

StandardServer 只是 Tomcat Server 的一个标准实现,相较于标准实现,它还有另外一个版本:EmbeddedTomcat,即内嵌的Tomcat。

初始化 Service.init()

​ 跟Server一样,Service的实现也叫StandardService,其初始化过程也是StandardService.initInternal()中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
protected void initInternal() throws LifecycleException {

super.initInternal();

if (engine != null) {
engine.init();
}

// Initialize any Executors
for (Executor executor : findExecutors()) {
if (executor instanceof JmxEnabled) {
((JmxEnabled) executor).setDomain(getDomain());
}
executor.init();
}

// Initialize mapper listener
mapperListener.init();

// Initialize our defined Connectors
synchronized (connectorsLock) {
for (Connector connector : connectors) {
connector.init();
}
}
}

​ 过程包括如下的几个核心点:

  1. 初始化 EngineEngine在整个Tomcat源码框架中的戏份不多,当用户的请求到来时,到MapperData中获取对应的Host,并让请求通过HostPipeline

  2. 初始化 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"/>
    -->
  3. 初始化 MapperListener,这一步非常的简单,很容易就被忽略,但其实 非常非常 的重要,如果想要弄清楚Tomcat是如何分发路由请求,一定要了解这个组件。我的另外一篇文章Tomcat的路由游戏对这个有详细的讲解。

  4. 初始化各个Connector,这个组件非常重要,这个我们单独用一节来讲解。

初始化 Connector.init()

Connector是整个Tomcat中比较重要的一个组件,甚至说是最核心的组件都不为过,它的主要任务是负责接收浏览器发过来的TCP连接请求,创建 RequestResponse 对象来封装接收、发送数据,然后把数据交给Container进行处理。

​ 如果你仔细查看过server.xml的话,会发现一个Service节点下可以有很多的Connector,每个Connector都可以监听不同的端口,采用不同的协议来和用户交互。如下所示(我们在8080端口监听普通的请求,在 8443 监听 SSL 请求):

1
2
3
4
5
6
7
8
9
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />

<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="150" SSLEnabled="true">
<SSLHostConfig>
<Certificate certificateKeystoreFile="conf/localhost-rsa.jks" type="RSA" />
</SSLHostConfig>
</Connector>

​ 当然以上这些工作都是在它启动之后,它的初始化过程其实是比较简单的,代码同样也是在Connector.initInternal()中,核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
protected void initInternal() throws LifecycleException {

super.initInternal();

if (protocolHandler == null) {
throw new LifecycleException(
sm.getString("coyoteConnector.protocolHandlerInstantiationFailed"));
}

// Initialize adapter
adapter = new CoyoteAdapter(this);
protocolHandler.setAdapter(adapter);

// Make sure parseBodyMethodsSet has a default
if (null == parseBodyMethodsSet) {
setParseBodyMethods(getParseBodyMethods());
}

if (protocolHandler.isAprRequired() && !AprLifecycleListener.isAprAvailable()) {
throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerNoApr",
getProtocolHandlerClassName()));
}
if (AprLifecycleListener.isAprAvailable() && AprLifecycleListener.getUseOpenSSL() &&
protocolHandler instanceof AbstractHttp11JsseProtocol) {
AbstractHttp11JsseProtocol<?> jsseProtocolHandler =
(AbstractHttp11JsseProtocol<?>) protocolHandler;
if (jsseProtocolHandler.isSSLEnabled() &&
jsseProtocolHandler.getSslImplementationName() == null) {
// OpenSSL is compatible with the JSSE configuration, so use it if APR is available
jsseProtocolHandler.setSslImplementationName(OpenSSLImplementation.class.getName());
}
}

try {
protocolHandler.init();
} catch (Exception e) {
throw new LifecycleException(
sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
}
}

​ 就是根据不同的Connector配置进行必要的设置(如SSL的支持等),之后就交给各个ProtocolHandler去根据不同的协议进行相应的初始化,常见的如Http11NioProtocolAjpNioProtocol等。

服务启动 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public void lifecycleEvent(LifecycleEvent event) {
// Identify the context we are associated with
try {
context = (Context) event.getLifecycle();
} catch (ClassCastException e) {
log.error(sm.getString("contextConfig.cce", event.getLifecycle()), e);
return;
}

// Process the event that has occurred
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {

// 这里开始进行配置
configureStart();

} else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
beforeStart();
} else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
// Restore docBase for management tools
if (originalDocBase != null) {
context.setDocBase(originalDocBase);
}
} else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
configureStop();
} else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
init();
} else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
destroy();
}

}

​ 一路跟踪的路径如下:webConfig() -> configureContext(webXml) -> context.addServletMappingDecoded(urlPattern, jspServletName, true); 这就是我们在web.xml中配置的所有的Servlet注册过程。

优雅的关闭:ShutdownHook

​ 回过头去看Catalina.start()知道,在完成 Server.start() 后还有一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Register shutdown hook
if (useShutdownHook) {
if (shutdownHook == null) {
shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);

// If JULI is being used, disable JULI's shutdown hook since
// shutdown hooks run in parallel and log messages may be lost
// if JULI's hook completes before the CatalinaShutdownHook()
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager) logManager).setUseShutdownHook(false);
}
}

​ 这是一段标准的Java钩子函数的注册过程,Runtime.getRuntime().addShutdownHook注册的钩子将会在JVM退出时执行。

​ Tomcat在这里注册的钩子用于保证优雅的关闭Tomcat容器,包括关闭打开的JNDI资源,数据库驱动,关闭线程池,清理fork出来的子线程等等。部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* Clear references.
*/
protected void clearReferences() {

// If the JVM is shutting down, skip the memory leak checks
if (skipMemoryLeakChecksOnJvmShutdown
&& !resources.getContext().getParent().getState().isAvailable()) {
// During reloading / redeployment the parent is expected to be
// available. Parent is not available so this might be a JVM
// shutdown.
try {
Thread dummyHook = new Thread();
Runtime.getRuntime().addShutdownHook(dummyHook);
Runtime.getRuntime().removeShutdownHook(dummyHook);
} catch (IllegalStateException ise) {
return;
}
}

// De-register any remaining JDBC drivers
clearReferencesJdbc();

// Stop any threads the web application started
clearReferencesThreads();

// Clear any references retained in the serialization caches
if (clearReferencesObjectStreamClassCaches) {
clearReferencesObjectStreamClassCaches();
}

// Check for leaks triggered by ThreadLocals loaded by this class loader
checkThreadLocalsForLeaks();

// Clear RMI Targets loaded by this class loader
if (clearReferencesRmiTargets) {
clearReferencesRmiTargets();
}

// Clear the IntrospectionUtils cache.
IntrospectionUtils.clear();

// Clear the classloader reference in common-logging
if (clearReferencesLogFactoryRelease) {
org.apache.juli.logging.LogFactory.release(this);
}

// Clear the classloader reference in the VM's bean introspector
java.beans.Introspector.flushCaches();

// Clear any custom URLStreamHandlers
TomcatURLStreamHandlerFactory.release(this);
}

​ 那么,Tomcat是如何知道容器中有哪些由于用户的请求而fork出来的线程,并在关闭时及时的清理呢?可以关注我的另外一篇文章:Tomcat源码解读:Connector之一个请求的完整历程

总结

​ 至此,整个Tomcat的启动过程结束,可以看出还是比较 简洁 的,结构非常的清晰,遵循Lifecycle的生命周期,在initstart阶段就完整了整个容器的构建过程。

​ 通过这个过程的分析我们可以学习到怎么构建一个层次分明的容器关系,怎么利用 Lifecycle 模式分而治之地编码,怎么优雅地关闭容器。同时也了解到Tomcat各个组件的关系及用途,能够更好地优化Tomcat的运行性能,进而定制更适合自己需要的Tomcat。