前言
借着上次对Tomcat类加载机制的分析,就想着看都看了,何不再看看Tomcat内部的实现原理和架构设计,向优秀的源码学习。Tomcat相较于其他的web容器,比如Jetty,要更加的复杂,内部应用了很多优秀的设计模式和思想,一上来就一头扎进源码进行分析并不是特别好的学习方式,因此,本文在借鉴其他文章、书籍的基础上,从大家都熟悉的server.xml
配置文件入手,循序渐进的分析。文章主要的篇章布局是:
- 分析
server.xml
配置文件中的常用标签,引出Tomcat中的对应组件- 给出比较全面的Tomcat架构图,和上面的分析相互印证
- 从源码的角度分析组件是如何被Tomcat所加载的
由于Tomcat相关的内容比较繁杂,很难在一篇文章内讲清楚所有重要的内容,因此,本文重点在于核心组件的“静态”分析,在代码层面类与类之间是如何组合的。本文依然依赖于Tomcat7版本的源码,读者需要先搭建对应的测试环境
1. Tomcat配置文件server.xml
标签解析
相信大部分Javaer都用过Tomcat作为web容器,那么对于其中的server.xml
肯定也不陌生,其中配置了Tomcat启动要加载的各种组件
<Server port="8005" shutdown="SHUTDOWN">
<Listener className="org.apache.catalina.startup.VersionLoggerListener"/>
<!-- Security listener. Documentation at /docs/config/listeners.html
<Listener className="org.apache.catalina.security.SecurityListener" />
-->
<!--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 -->
<Listener className="org.apache.catalina.core.JasperListener"/>
<!-- 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"/>
<!-- Global JNDI resources
Documentation at /docs/jndi-resources-howto.html
-->
<GlobalNamingResources>
<!-- Editable user database that can also be used by
UserDatabaseRealm to authenticate users
-->
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml"/>
</GlobalNamingResources>
<!-- A "Service" is a collection of one or more "Connectors" that share
a single "Container" Note: A "Service" is not itself a "Container",
so you may not define subcomponents such as "Valves" at this level.
Documentation at /docs/config/service.html
-->
<Service name="Catalina">
<!--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"/>
-->
<!-- A "Connector" represents an endpoint by which requests are received
and responses are returned. Documentation at :
Java HTTP Connector: /docs/config/http.html (blocking & non-blocking)
Java AJP Connector: /docs/config/ajp.html
APR (HTTP/AJP) Connector: /docs/apr.html
Define a non-SSL HTTP/1.1 Connector on port 8080
-->
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"/>
<!-- A "Connector" using the shared thread pool-->
<!--
<Connector executor="tomcatThreadPool"
port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
-->
<!-- Define a SSL HTTP/1.1 Connector on port 8443
This connector uses the BIO implementation that requires the JSSE
style configuration. When using the APR/native implementation, the
OpenSSL style configuration is required as described in the APR/native
documentation -->
<!--
<Connector port="8443" protocol="org.apache.coyote.http11.Http11Protocol"
maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
clientAuth="false" sslProtocol="TLS" />
-->
<!-- Define an AJP 1.3 Connector on port 8009 -->
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443"/>
<!-- An Engine represents the entry point (within Catalina) that processes
every request. The Engine implementation for Tomcat stand alone
analyzes the HTTP headers included with the request, and passes them
on to the appropriate Host (virtual host).
Documentation at /docs/config/engine.html -->
<!-- You should set jvmRoute to support load-balancing via AJP ie :
<Engine name="Catalina" defaultHost="localhost" jvmRoute="jvm1">
-->
<Engine name="Catalina" defaultHost="localhost">
<!--For clustering, please take a look at documentation at:
/docs/cluster-howto.html (simple how to)
/docs/config/cluster.html (reference documentation) -->
<!--
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>
-->
<!-- Use the LockOutRealm to prevent attempts to guess user passwords
via a brute-force attack -->
<Realm className="org.apache.catalina.realm.LockOutRealm">
<!-- This Realm uses the UserDatabase configured in the global JNDI
resources under the key "UserDatabase". Any edits
that are performed against this UserDatabase are immediately
available for use by the Realm. -->
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<!-- SingleSignOn valve, share authentication between web applications
Documentation at: /docs/config/valve.html -->
<!--
<Valve className="org.apache.catalina.authenticator.SingleSignOn" />
-->
<Context path="" docBase="www/" reloadable="true" />
<!-- Access log processes all example.
Documentation at: /docs/config/valve.html
Note: The pattern used is equivalent to using pattern="common" -->
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log." suffix=".txt"
pattern="%h %l %u %t "%r" %s %b"/>
</Host>
</Engine>
</Service>
</Server>
从上面标准的server.xml
中可以看出,<Server>
作为顶层标签,下面的子标签有<Listener>
、<GlobalNamingResources>
、<Service>
三个,我们猜测Tomcat中必定有一种类对应<Server>
标签,同时也会存在三种类 (为什么不说三个类,因为可能存在一对多的关系) 对应下面的子标签,而父子之间的关系可能通过组合的关系联系在一起。同样的,也可以推理出存在<Resource>
、<Executor>
、<Connector>
、<Cluster>
、<Realm>
、<Host>
、<Context>
、<Valve>
这几个标签对应的类,他们之间的关系也可以根据标签之间的“父子”关系推断出来
2. Tomcat整体架构图
我找了一张比较完整的Tomcat架构图,通过真正的抽象化架构来评判上面我们推断的合理性,有什么遗漏的地方,或错误的地方
从图中可以看到,大部分的组件都与
server.xml
中标签有着对应关系,比如<server>
对应图中的Server组件,该组件是Tomcat的顶层组件,其中包含一个或者多个Service组件,正如<Server>
中包含一个或多个<Service>
子标签一样,但即便如此,我们仍需要着重看一下代码层面的实现,毕竟这才是验证理论最可靠的途径
3. 代码层面实现
首先我们要看一下server.xml
是如何被加载进Tomcat容器中,在违反ClassLoader双亲委派机制三部曲第二部——Tomcat类加载机制中,我们知道了Tomcat的是通过Bootstrap.java
的main(String args[])
启动的,代码清单1
public static void main(String args[]) {
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
// (1)
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
// (2)
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 (command.equals("startd")) {
args[args.length - 1] = "start";
daemon.load(args);
daemon.start();
} else if (command.equals("stopd")) {
args[args.length - 1] = "stop";
daemon.stop();
}
// (3)
else if (command.equals("start")) {
daemon.setAwait(true);
daemon.load(args);
daemon.start();
} else if (command.equals("stop")) {
daemon.stopServer(args);
} else if (command.equals("configtest")) {
daemon.load(args);
if (null==daemon.getServer()) {
System.exit(1);
}
System.exit(0);
} else {
log.warn("Bootstrap: command \"" + command + "\" does not exist.");
}
} catch (Throwable t) {
// Unwrap the Exception for clearer error reporting
if (t instanceof InvocationTargetException &&
t.getCause() != null) {
t = t.getCause();
}
handleThrowable(t);
t.printStackTrace();
System.exit(1);
}
}
注释1处,main
方法内首先调用了init()
方法,在该方法中使用反射创建了org.apache.catalina.startup.Catalina
类的实例,并将该实例赋值给了Bootstrap
类中的catalinaDaemon
实例,默认启动Tomcat容器流程会走到注释3处,调用daemon.load(args)
方法,这里的daemon
实例其实就是注释2处的Bootstrap
自己的实例,我们接着看load(String[])
方法
该方法的主要逻辑就是通过反射调用了成员变量
catalinaDaemon
的load(String args[])
方法,上面说过,catalinaDaemon
实际上就是Catalina.class
的实例对象,因此,最终调用了Catalina
类的load()
方法,代码清单2
public void load() {
long t1 = System.nanoTime();
initDirs();
// Before digester - it may be needed
initNaming();
// (1)
// Create and execute our Digester
Digester digester = createStartDigester();
InputSource inputSource = null;
InputStream inputStream = null;
File file = null;
try {
try {
file = configFile();
inputStream = new FileInputStream(file);
inputSource = new InputSource(file.toURI().toURL().toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail", file), e);
}
}
// 省略其他代码.....
try {
inputSource.setByteStream(inputStream);
// (2)
digester.push(this);
// (3)
digester.parse(inputSource);
} catch (SAXParseException spe) {
log.warn("Catalina.start using " + getConfigFile() + ": " +
spe.getMessage());
return;
} catch (Exception e) {
log.warn("Catalina.start using " + getConfigFile() + ": " , e);
return;
}
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// Ignore
}
}
}
getServer().setCatalina(this);
// Stream redirection
initStreams();
// Start the new server
try {
// (4)
getServer().init();
} catch (LifecycleException e) {
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
throw new java.lang.Error(e);
} else {
log.error("Catalina.start", e);
}
}
long t2 = System.nanoTime();
if(log.isInfoEnabled()) {
log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms");
}
}
Tomcat底层使用SAX来对xml文件进行解析,具体来说,注释1处createStartDigester()
方法的目的是为解析server.xml
创建特定的“摘要”,Tomcat采用Digester.java
来封装对server.xml
文件中所有标签的解析规则,每一种规则都是Rule
接口的实现,Digester.java
中的startDocument()
、startElement(String,String,String,Attributes)
、endDocument()
、endElment(String,String,String)
等方法都是标准的SAX解析模块,分别用于文档的开始结束、元素的开始结束。为了突出重点,该流程我们采用一个为<Server>
标签设置解析规则的例子说明
红框内的代码实际上为解析<Server>
标签创建了三个规则ObjectCreateRule
、SetPropertiesRule
和SetNextRule
,并指明了<Server>
对应对象的实例为org.apache.catalina.core.StandardServer
,这三个规则最终会被放在规则父类RuleBase
类的缓存HashMap<String,List<Rule>> cache
中,而Digester
又持有该类的实例,也就是说Digester
最终会装载解析xml文件所需的所有规则
我们回到代码清单2中的注释2,Digester
做了一个类似压栈的操作,将当前的Catalina
对象压入Catalina
类中的ArrayStack<Object> stack
中,根据栈先进后出的特性可知该Catalina
对象必定会最后一个弹栈,而栈中存放的其他对象实际上就是上面对应标签的java类实例,举个例子,如果server.xml
中标签的结构为
<Server>
<Service>
</Service>
</Server>
那么最后栈中的结构必然是先入栈Catalina
实例,然后是<Server>
标签对应类的实例,栈顶的是<Service>
标签的实例。为什么要用这种设计思路存放标签对应的类实例,我理解可以想一想SAX方式解析xml文件的特点,SAX对xml文件边扫描边解析,自顶向下依次解析,可以看成是深度优先遍历的一种变体,该特性在数据结构的层面上正好用栈完美诠释,这里又为什么要将“自己”压入栈底,答案随着分析的深入自会揭晓,现在只需要记住
代码清单2中的注释3,此处通过摘要类的实例对已经加载为输入流形式的server.xml
进行了解析,上面说过Digester
作为SAX的解析类,当解析到Docuemt开始会调用startDocument()
方法,解析到Element开始会调用startElement()
方法,我们来看一下
因为SAX解析会将每一个标签映射成一个Element,红框内的代码主要是在标签解析的时候筛选出之前为对应标签配置的规则,比如当解析到
<Server>
标签时,会从上面所说的标签cache中得到为其所配置的ObjectCreateRule
、SetPropertiesRule
和SetNextRule
三个规则,然后依次调用对应规则的begin
方法,同样的Digester
在解析到标签的结尾时会调用endElment()
方法,在该方法中也会有遍历所有规则的流程,与处理标签开始不同的是,结束时会依次调用规则的end
方法,这里我们仅以ObjectCreateRule
的begin
方法举例
图中
className
和realClassName
实际上就是图3中的org.apache.catalina.core.StandardServer
,所以<Server>
标签实际上就生成了StandardServer.java
的实例,从而建立了标签和类的对应关系,同时将StandardServer
实例压栈代码清单2中的注释4代码主要进行各个容器的初始化工作,具体的初始化流程在下一篇讲述容器生命周期的文章中详述,这里一笔带过。但是有一个问题,就是这里的
getServer()
方法返回了Catalina
类中的protected Server server = null;
,这个Server实际上就是上面创建的<Server>
标签对应的实例StandardServer
,问题是Tomcat是何时将这个初始值为null的Server赋值的呢?有人肯定会说肯定会调用该变量的
setServer(Server)
方法啊,在Catalina
类中确实存在setServer(Server)
方法,但查询其调用链时发现该方法并没有被直接调用过,那这个Server是如何被赋值的呢?我们要重新看看在解析<Server>
标签时Rule
起了什么作用,ObjectCreateRule
主要生成标签对应的类的实例,并将其压栈;SetPropertiesRule
主要用于标签参数的解析;SetNextRule
处理父子标签对应类方法的调用,建立标签实体之间的关联
为了调试方便,我们对
server.xml
中的内容进行了修改,只保留了顶层的<Server>
标签,从调试截图可见,此时栈顶元素为StandardServer
,栈底元素为Catalina
,待调用的方法名称为setServer
,最后通过内省工具类Object callMethod1(Object, String, Object, String, ClassLoader) throws Exception
完成了层级关联关系的映射,图中就是用Catalina
实例调用了他的setServer(Server)
方法,其传入的Server就是StandardServer
的实例。至此完成了server.xml
文件中组件的解析,最后我们以<Server>
标签和<Service>
标签为例看一看代码层面的表现形式
Tomcat中各组件在类层面上的关系基本如图7、图8所示,层级关系表现为双向的关联关系,数量关系表现为数组对象的引用,其主要的思想还是内含在对
server.xml
解析的过程中