Tomcat整体架构浅析
1. 整体结构
架构图:
1.1 各组件解释:
从顶层开始:
Server是Tomcat的最顶层元素,是service的集合,即可包含多个service,Server控制整个Tomcat的生命周期。
Service由一个Container和多个Connector组成(或者说由Connector,Engine和线程池[可选]组成),形成一个独立完整的处理单元,对外提供服务。
一般情况下我们并不需要配置多个Service,conf/server.xml默认配置了一个“Catalina”的。
Tomcat将Engine,Host,Context,Wrapper统一抽象成Container。
Connector接受到请求后,会将请求交给Container,Container处理完了之后将结果返回给Connector
下面看Container的结构:
(1)Engine:没有父容器,一个 Engine代表一个完整的 Servlet 引擎,它接收来自Connector的请求,并决定传给哪个Host来处理,Host处理完请求后,将结果返回给Engine,Engine再将结果返回给Connector。
(2)Host:Engine可以包含多个Host,每个Host代表一个虚拟主机,这个虚拟主机的作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们,每个虚拟主机对应的一个域名,不同Host容器接受处理对应不同域名的请求。
(3)Context:Host可以包含多个Context,Context是Servlet规范的实现,它提供了Servlet的基本环境,一个Context代表一个运行在Host上的Web应用
(4)Wrapper: Context可以包含多个Wrapper, Wrapper 代表一个 Servlet,它负责管理一个 Servlet,包括的 Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。
组件包含关系
Standard*XXXX*是组件接口的默认实现类。
其它组件
Tomcat 还有其它组件,如安全组件 security、logger、session、naming 等其它组件。这些组件共同为 Connector 和 Container 提供必要的服务。
1.2 组件的生命线Lifecycle
Tomcat中很多组件具有生命周期,如初始化、启动、关闭,这些组件的生命周期具有共性,因此Tomcat中将其抽象为接口Lifecycle,来控制组件的生命周期,它通过 事件机制 实现各个容器间的内部通讯。
Lifecycle接口的方法:
继承关系图:
StandardServer,StandardService,Connector和上面4个容器等很多组件都实现了Lifecycle,组件实现这个接口就可以统一被拥有它的组件控制了,这样一层一层的直到一个 *的组件 就可以控制 Tomcat 中所有组件的生命周期,这个最高的组件就是 Server。
2. 启动流程
查看Tomcat的启动脚本bin/catalina.sh(Linux平台)就会发现,Tomcat的“起点”是org.apache.catalina.startup.BootStrap的main方法,catalina.sh脚本的参数(start/stop)都会传入BootStrap的main函数。而BootStrap进一步根据参数调用org.apache.catalina.startup.Catalina的对应方法来完成启动过程的任务。
BootStrap类和实例分别有两个重要的属性:
private static Bootstrap daemon = null;
private Object catalinaDaemon = null;
很明显,Bootstarp是一个单例的守护对象负责引导这个Tomcat启动,而catalinaDaemon实际是Catalina类实例,对应于bin/catalina.sh脚本负责实际的启动和停止过程。
这个问题同样很复杂,包括如何读取和解析配置,创建和启动组件的顺序,以及这个环节中重要的事件和对应的处理等等,按照时间顺序分析启动过程的同时我们可以一个个解决这些问题。
2.1 初始化类加载器
在BootStrap.main函数的第一个步骤是进行初始化类加载,包括commonLoader,catalinaLoader和sharedLoader,默认情况这三个引用指向同一个commonLoader实例,后面专门总结Tomcat类加载器结构的时候在详细分析。
BootStrap.init方法:
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;
}
2.2 加载和配置的读取&解析
对应BootStrap.main方法中:
} else if (command.equals("start")) {
daemon.setAwait(true);
daemon.load(args);
daemon.start();
}
(1)setAwait(true),最终是设置Server的await标志,作用是在启动工作完成后,StandardServer会调用其await()方法阻塞当前线程(也是启动工作线程),等待关闭时才中断这个线程的等待状态。
(2)load(args);实际上会调用Catalina.load方法,进行:
验证java.io.tmpdir所指缓存目录是否可用;
读取和解析server.xml;
初始化日志输入,输出流(正常,异常);
开始Tomcat初始化过程,从Server.init()开始;
(3)start(),调用Catalina.start,将开始Tomcat组件和容器的启动,从Server.start()开始;
先看看加载和解析的过程:
问题:Tomcat加载和解析XML的方式?
首先,使用过Tomcat的人应该知道,CATALINA_BASE目录(默认是Tomcat的安装目录)下conf中有很多Tomcat配置文件:
server.xml(Tomcat主配置文件);
web.xml(适用于所有Web应用的servlet规范配置文件);
tomcat-users.xml(用户认证,角色等信息,UserDatabaseRealm相关);
catalina.policy(Java安全防护策略文件);
context.xml(默认context设置,应用于所有部署内容);
这里首当其冲应该是server.xml,Tomcat中一个读取和解析XML配置文件的主要方法是SAX,使用SAX而不是DOM是因为SAX的事件驱动型型特点,这样可以一边扫描一边进行响应的配置。Tomcat解决这一问题的主要逻辑在org.apache.tomcat.util.digester包中。
Tomcat在应用SAX事件驱动型解析中有几个重要的概念:
(1)Handler,在解析过程中不同的事件可以调用Handler对应的回调方法(包括EntityResolver, DTDHandler, ContentHandler, ErrorHandler),org.apache.tomcat.util.digester.Digester是具体的实现;
(2)Rule,这是Tomcat中定义的类,虽然Handler提供了不同回调方法可以实现,但是Tomcat没有定义大量的Handler,而是一次解析一个xml文件只定义单个Digester实例,而将“遇到一个创建一个StandardServer对象”这样的需求抽象成Rule,一个Digester包含解析所需的所有Rule(聚合)。每个Rule可以选择性定义begin,body,end,finish这些回调。
(3)Digester除了用数组保存既定的规则外,还通过Stack保存解析过程创建的对应的组件对象,因为嵌套标签解析过程中begin-body(包含子标签)-end这种递归嵌套方式和方法调用一样适合用LIFO的栈来表示。
比如,Catalina在创建解析server.xml的Digester实例的createStartDigester()方法中添加了这样一个条规则:
digester.addObjectCreate("Server/Service",
"org.apache.catalina.core.StandardService",
"className");
digester.addSetProperties("Server/Service");
digester.addSetNext("Server/Service",
"addService",
"org.apache.catalina.Service");
意为:遇到一个包含的标签,首先创建一个StandardService对象(通过反射),继续处理它的子标签(递归),子标签处理完后。对其父元素也就是Server对象调用addService方法建立对象之间的父子组合关系。最后该解析完,对应的组件对象出栈。上述代码中addObjectCreate一般用于在匹配标签开始解析时进行创建对象,addSetNext一般在标签解析最后建立其与父标签组件对象的父子组合关系。
如果里完整看完Catalina.createStartDigester()就会明白我为什么要费这么多口舌说明这个问题,因为实际上在 server.xml定义的重要组件包括:Server,Service,Engine,Host,Context,以及Valve,Listener等等都是以这种方式在解析的过程中创建和组配的。结合我们在上面Container结构中的分析,包括Pileline,Valve与Container的联系也可以在此建立好,因此Tomcat在容器和其他组件进行初始化之前,已经构建起了一个完整的对象网络。
2.3 容器初始化
根据上面的说明,我们知道现在组件已经创建好了,可以开始初始化了,Server.init就是起点,这是在daemon.load()中进行的。在上两个小节,将会提到Tomcat中组件一般实现Lifecycle,骨架类LifecycleBase在实现init,start,stop,destroy的基本逻辑之外定义了initIntertal等扩展的回调方法,这样大部分组件只要实现initIntertal等方法就可以了。
下面我沿着Server—>Service->Container(Engine—>Host—>Context—>Wrapper—>Servlet)这一核心顺序一一总结Tomcat初始化的过程。
2.3.1 StandardServer.initInternal():
(1)创建和注册(注册到MBeanServer)全局的StringCache;
(2)初始化GlobalNamingResources;
(3)初始化该Server包含的所有service组件(虽然通常都只有一个名为“Catalina”的service);
2.3.2 StandardService.initInternal():
这一层其实包含了很多重要组件的初始化:Container,Executor,MapperListener,Connector;
(1)初始化容器,从容器的最外层(Engine)开始,一层层开始;
(2)如果定义了org.apache.catalina.Executor,初始化Executor。还是说明一下,它实现了J.U.C中的Executor,定义一个为所有Connector共享的线程池(因此在server.xml中Executor必须定义在Connector,因为前面提到了解析server.xml使用SAX的方式);
(3)初始化mapperListener,MapperListener是Tomcat中用来保存整个容器必要结构信息用于将请求URL映射到对应容器;
(4)初始化Connector,这个过程会初始化每个Connector包含的ProtocolHandldr等组件,让连接处理部分做好准备;
2.3.3 容器初始化
结合2.1节我们可以知道,初始化和启动调用栈包含LifecyleBase-LifecyleMBeanBase-具体容器类几个层次,并且在对应的时间点/状态变化点通知LifecycleLsitener:
(1)首先是LifycycleBase的init()方法:
LifecycleState.INITIALIZING——>initInternal()——>LifecycleState.INITIALIZED;
(2)initInternal调用栈从LifecycleMBeanBase开始,LifecycleMBeanBase.initInternal()将容器注册为MBean;
(3)再来看ContainerBase.initInternal,在2.1节我们提到过它负责创建和设置用于执行子容器启动任务的线程池,下面是其代码:
protected void initInternal() throws LifecycleException {
reconfigureStartStopExecutor(getStartStopThreadsInternal());
super.initInternal();
}
private void reconfigureStartStopExecutor(int threads) {
if (threads == 1) {
if (!(startStopExecutor instanceof InlineExecutorService)) {
startStopExecutor = new InlineExecutorService();
}
} else {
if (startStopExecutor instanceof ThreadPoolExecutor) {
((ThreadPoolExecutor) startStopExecutor).setMaximumPoolSize(threads);
((ThreadPoolExecutor) startStopExecutor).setCorePoolSize(threads);
} else {
BlockingQueue<Runnable> startStopQueue = new LinkedBlockingQueue<>();
ThreadPoolExecutor tpe = new ThreadPoolExecutor(threads, threads, 10,
TimeUnit.SECONDS, startStopQueue,
new StartStopThreadFactory(getName() + "-startStop-"));
tpe.allowCoreThreadTimeOut(true);
startStopExecutor = tpe;
}
}
}
(4)接下来看具体的容器类,StandardHost,StandardWrapper没有覆盖父类的行为,StandardEngine也只是简单准备了下Realm,StandardContext中主要是:将Context包含的NamingResoource注册到MBeanServer以及WebResourceRoot启动(如果存在的话);在2.1节说明容器的后台任务时,我们提到WebResourceRoot会在后台线程中周期性的清除过期缓存。
Resources:元素可以定义在当中。Tomcat 8的官方配置手册是这样解释的:
The Resources element represents all the resources available to the web application. This includes classes, JAR files, HTML, JSPs and any other files that contribute to the web application.
一般在Context含有未存储在Tomcat的本机硬盘上的资源或者对资源的缓存等细节有定制需求时,才会需要此元素。不定义该元素,将会使用基于默认文件系统(项目根目录)的WebResourceRoot对象。
Tomcat 8相较之前版本,对<Resources>进行较大幅度的修改,对该元素的实现类是org.apache.catalina.WebResourceRoot的子类(一般是org.apache.catalina.webresources.StandardRoot),而不是原来的javax.naming.directory.DirContext。Tomcat 8在此基础上为<Resources>定义了很多新的属性,包括缓存的相关细节以及一些加载顺序有关的内嵌标签,具体可以参考文章开头的参考资料(8)Tomcat 8官方配置手册。
在StandardContext初始化完成后,会通知注册的LifecycleListener,其中包括ContextConfig,调用ContextConfig.init()解析/conf目录下的context.xml以及Context自身的配置文件,解析方式和前面servlet.xml一样,基于org.apache.tomcat.util.digester.Digester一边读取解析,一边构建对象网络。
2.4 容器启动
BootStrap在load()加载完的下一个步骤就是启动了daemon.start(),启动同样是从Server开始:
2.4.1 StandardServer.startInternal()
启动全局NamingResources;启动所有Service(一般就是名为catalina的Service);
2.4.2 StandardService.startInternal():
和初始化的顺序相似:
(1)启动Container,container.start();
(2)启动Executor;
(3)启动mapperListener;
(4)启动所有的Connector;
Executor,MapperListener,Connector都是接收请求直接相关的,其中Executor负责为Connector处理请求提供共用的线程池,MapperListener负责将请求映射到对应的容器中,Connector负责接收和解析请求,具体的过程在后面2.4小节详细的探讨。这里所有Connector启动完成后,Tomcat就准备好可以接受处理请求。当然我们同样要深入看看容器的启动过程。
2.4.3 容器启动
首先看看ContainerBase.startInternal(),2.1节分析ContainerBase结构时,已经提过一项基础的工作就是通过线程池执行子容器启动任务。按照顺序基本的工作包括:
(1)Cluster服务启动;
(2)Realm服务启动;
(3)子容器的启动;
(4)Pipeline的启动(Pipeline进一步启动对应Valve链上所有的Valve);
(5)通知执行STARTING对应的Listener;
(6)后台任务共享线程的启动;
进而来看看具体容器类的启动过程:
Engine(没有额外的工作)——>Host(Set error report valve);
接下来是Context,查看StandardContext我们可以看到直接对应于一个Web应用的Context的启动过程是很复杂的,而且它并没有调用super.startInternal(),在上篇已经较为详细的总计,这里再系统的顺利一下:
(1)创建读取资源文件的对象:如果我们没有上面在初始化过程中提到的元素,将会创建一个默认的StandardRoot;
(2)创建ClassLoader对象,为了实现不同应用类的隔离,每个Context有自己的WebappLoader,创建对应的WebappClassLoader;
(3)设置应用的工作目录;
(4)启动相关辅助类:Logger,Cluster,Realm;
(5)创建会话管理器;
(6)通知ContextConfig读取和解析Web应用web.xml和注解;
调用过程:
StandardContext.startInternal()触发通知fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);——>ContextConfig.configureStart()——>通过WebXml读取web.xml配置,扫描读取注解配置——>ContextConfig根据读取到的ServletDef创建StandardWrapper和FilterDef,Listener等组件配置信息一起注入Context中;
之后返回到StandardContext.startInternal(),StandardContext将按照在上篇中提到的顺序加载和初始化各个组件:
(7)启动子容器,也就是上一步创建的所有StandardWrapper;
(8)启动Pipeline;
(9)启动会话管理器Manager;
(10)获取ServletContext并设置必要的参数,ServletContext在Tomcat中的内部表示即ApplicationContext,返回到Servlet中的是它的门面对象ApplicationContextFacade;
(11)调用Initializer的onStartup;
(12)创建Context中配置的Listener;
(13)创建和初始化配置的Filter;
(14)创建和初始化loadOnStartup大于等于0的Servlet,StandardWrapper的门面类StandardWrapperFacade作为ServletConfig传入Servlet的init方法;
2.5 启动时序图
补充如下:
service的initInternal方法中调用 container.init() 是如何层级调用Engine,Host,Context,Wrapper的初始化方法?
StandardService中container属性就是StandardEngine,这个init只将StandardEngine进行了初始化。Host,Context,Wrapper的初始化在哪里?在调用Host的start时,进行初始化if (state.equals(LifecycleState.NEW)) {init();}。
同上,service的startInternal调用container.start,调用了Engine,Host的startInternal,Context的start在哪里调用了?
Host启动时触发事件,HostConfig监听到STARTING事件后会调用Context.start。
在tomcat7中,mapperListener的初始化和启动是封装在Connector中的。
关于Connector初始化和启动的更多细节,可参考本人另一篇blog http://blog.csdn.net/cx520forever/article/details/52198050
3. pipeline valve机制
3.1名词解释
pipeline 管道,可以比作车间生产线,在这里可认为是容器的逻辑处理总线
valve 阀门,可以比作生产线上的工人,负责完成各自的部分工作。
生产线(pipeline)可以配置其工人的位置。
与车间生产线不同的是,tomcat中pipeline的不同是valve拿到输入,处理完成后,需要将输出返回给调用方。
3.2总体分析
四个基本容器对象里面都有一个pipeline及valve模块,是容器类必须具有的模块,对象生成时set该属性。Pipeline就像是每个容器的逻辑总线。在pipeline上按照配置的顺序,加载各个valve。通过pipeline完成各个valve之间的调用,各个valve实现具体的应用逻辑。
tomcat组件图:
从上图中看到,在Connector接收到一次连接并转化成HttpServletRequest请求对象后,请求传递如下:
Connector–>Engine的Pipeline的ValveA中–>Engine Valve–>Host Pipeline的Error Report Valve和Host Value–>Context Valve–>Wrapper Valve中,在这里会经过一个过滤器链(Filter Chain)–>Servlet中。
Servlet处理完成后一步步返回,最后Connector拿到response。
3.3 接口及默认实现
3.3.1 pipeline
接口中定义的方法:
一个pipeline包含多个Valve,这些阀共分为两类,一类叫基础阀(通过getBasic、setBasic方法调用),一类是普通阀(通过addValve、removeValve调用)。管道都是包含在容器中,所以有getContainer和setContainer方法。一个管道一般有一个基础阀(通过setBasic添加),可以有0到多个普通阀(通过addValve添加)。
isAsyncSupported:当管道中的所有阀门都支持异步时返回ture,否则返回false
该接口的标准实现是:org.apache.catalina.core.StandardPipeline
Engine、Host、Context及Wrapper的pipeline属性都继承自父类ContainerBase.
3.3.2 Valve
接口方法:
重点关注setNext、getNext、invoke这三个方法,通过setNext设置该阀的下一阀,通过getNext返回该阀的下一个阀的引用,invoke方法则执行该阀内部自定义的请求处理代码。
ValveBase:是Valve接口的基本实现
四大容器类r都有各自缺省的标准valve实现。它们分别是
StandardEngineValve:StandardEngine中的唯一阀门,主要用于从request中选择其host映射的Host容器StandardHost。
StandardHostValve:StandardHost中最后的阀门,主要用于从request中选择其context映射的Context容器StandardContext以及访问request中的Session以更新会话的最后访问时间。
StandardContextValve:StandardContext中的唯一阀门,主要作用是禁止任何对WEB-INF或META-INF目录下资源的重定向访问,对应用程序热部署功能的实现,从request中获得StandardWrapper。
StandardWrapperValve:StandardWrapper中的唯一阀门,主要作用包括调用StandardWrapper的loadServlet方法生成Servlet实例和调用ApplicationFilterFactory生成Filter链。
3.3.3 自定义Valve
Valve实现了具体业务逻辑单元。可以定制化valve(实现特定接口),然后配置在server.xml里。每层容器都可以配置相应的valve,当只在其作用域内有效。例如engine容器里的valve只对其包含的所有host里的应用有效。
配置举例:
<Engine name="Catalina" defaultHost="localhost">
<Valve className="MyValve0"/>
<Valve className="MyValve1"/>
<Valve className="MyValve2"/>
……
<Host name="localhost" appBase="webapps">
</Host>
</Engine
当在server.xml文件中配置了一个定制化valve时,会调用pipeline对象的addValve方法,将valve以链表方式组织起来,代码如下;
@Override
public void addValve(Valve valve) {
// Validate that we can add this Valve
if (valve instanceof Contained)
((Contained) valve).setContainer(this.container);
// Start the new component if necessary
if (getState().isAvailable()) {
if (valve instanceof Lifecycle) {
try {
((Lifecycle) valve).start();
} catch (LifecycleException e) {
log.error("StandardPipeline.addValve: start: ", e);
}
}
}
// Add this Valve to the set associated with this Pipeline
//将配置的valve添加到链表中,并且每个容器的标准valve在链表的尾端
if (first == null) {
first = valve;
valve.setNext(basic);
} else {
Valve current = first;
while (current != null) {
if (current.getNext() == basic) {
current.setNext(valve);
valve.setNext(basic);
break;
}
current = current.getNext();
}
}
container.fireContainerEvent(Container.ADD_VALVE_EVENT, valve);
}
valve按照容器作用域的配置顺序来组织valve,每个valve都设置了指向下一个valve的next引用。同时,每个容器缺省的标准valve都存在于valve链表尾端,最后被调用。
Pipeline内部维护first和basic两个阀,其它相关阀通过getNext来获取。
标准valve的调用逻辑图:
从StandardEngineValve开始, 所有的基础阀的实现最后都会调用其下一级容器,所有的普通阀都会执行getNext().invoke(request, response);,一直到StandardWrapperValve,完成请求处理过程。因为Wrapper是对一个Servlet的包装,所以它的基础阀内部调用的过滤器链的doFilter方法和Servlet的service方法。
上述机制保证了请求传递到servlet去处理。
当采用tomcat默认初始配置时,Valve链如下:
这些阀门Valve通过invoke方法彼此串联起来,最终构成的执行顺序十分类似于一个管道。
4. Tomcat中的设计模式
4.1模板方法模式
把通用的骨架步骤抽象到父类中,子类去实现特定的某些步骤。
举例:
如LifecycleBase类中init和start方法,其中的nitInternal和startInternal方法是抽象方法,所有容易都直接或间接继承了LifecycleBase,在初始化和启动时被每个容器会调用其init和start方法,这些抽象方法都是在子类中实现的。
4.2责任链模式
Tomcat中的ApplicationFilterChain实现了Filter拦截和实际Servlet的请求,是典型的责任链模式。
Pipeline-Valve机制也是责任链模式,从Engine到Host再到Context一直到Wrapper都是通过一个链来传递请求。
4.3观察者模式
Tomcat通过LifecycleListener对组件生命周期组件Lifecycle进行监听,各个组件在其生命期中会有各种行为,而这些行为都会触发相应的事件,Tomcat就是通过侦听这些事件达到对这些行为进行扩展的目的。在看组件的init和start过程中会看到大量如:fireLifecycleEvent(CONFIGURE_START_EVENT, null);这样的代码,这就是对某一类型事件的触发,如果你想在其中加入自己的行为,就只用注册相应类型的事件即可。