Server、Service、Lifecycle
tomcat有一个配置文件server.xml
,我们的很多配置在该配置文件中配置,然后tomcat启动后读取到配置。
对于tomcat的架构,我们从server.xml
中也可见一斑。
下面是一个server.xml:
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
<!-- 实现JNDI
Global JNDI resources
Documentation at /docs/jndi-resources-howto.html
执行资源的配置信息
-->
<GlobalNamingResources>
<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>
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<Engine name="Catalina" defaultHost="localhost">
<Realm className="org.apache.catalina.realm.LockOutRealm">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<!--主机-->
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<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标签,代表着整个tomcat服务器,而server标签中可以配置GlobalNamingResources
标签,对应着JNDI的相关实现,还可以配置多个service
,而tomcat官方默认的service是Catalina
服务。而源码中恰好有一个类叫做org.apache.catalina.Server
。
再看这个类的方法,可以配置port,可以addService,可以设置GlobalNamingResources,所以猜测server标签对应着Server类。那么是不是Service类对应着service标签,是的,没错。
看org.apache.catalina.Service
类的方法:
通过addConnector
方法可以添加一个Connector
,译为连接器;通过addExecutor
方法可以添加一个Executor
,作为Service的线程池,此外同一个Service中的组件可以共享一个线程池(如果没有配置会自动创建默认的线程池);通过setContainer方法可以添加一个Engine
,译为引擎。而上述在Service中配置的三个组件:Connector、Executor、Engine,在我们的server.xml文件service标签中同样是可以配置的。
我们再看Connector、Executor、Engine、Service、Server类,这些类都有一个父接口Lifecycle
,译为生命周期,提供了四个声明周期方法,init初始化、start启动、stop停止、destroy销毁方法。
Container
我们知道一个Server中可以配置多个Service。这里我们先做一个小实验,修改server.xml。
<?xml version="1.0" encoding="UTF-8"?>
<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" />
<!-- 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" />
<!-- 实现JNDI
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>
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<Engine name="Catalina" defaultHost="localhost">
<Realm className="org.apache.catalina.realm.LockOutRealm">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<!--主机-->
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<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>
<Service name="wj">
<Connector port="8081" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8444" />
<Engine name="wj" defaultHost="localhost">
<Realm className="org.apache.catalina.realm.LockOutRealm">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<!--主机-->
<Host name="localhost" appBase="wj"
unpackWARs="true" autoDeploy="true">
<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>
这里我又配置了一个service,而这个service监听8081端口,旗下所有的请求处理都去wj目录下去找(因为我配置了appBase),并且主机是localhost(在Host标签中配置),然后新建一个index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
hello tomcat
</body>
</html>
目录层级如下:
启动tomcat后,直接访问localhost:8081/hello
,能直接访问到我写的index.html
。
Connector负责开启Socket并监听客户端请求、返回响应数据。当Connector收到请求后,会将请求交给Engine(引擎),Engine只负责请求的处理,并不需要考虑请求链接、协议的处理。
Engine中可以配置Host(虚拟主机),通过配置多个Host,我们就可以提供多个域名的服务。
注意到:Engine、Host的父接口是Container
容器,该类中有一个重要的方法:addChild
,通过此方法来设定组件的层级关系。
而Engine中的addChild方法:则是添加Host
@Override
public void addChild(Container child) {
if (!(child instanceof Host)) {
throw new IllegalArgumentException
(sm.getString("standardEngine.notHost"));
}
super.addChild(child);
}
Host中的addChild:是添加Context
@Override
public void addChild(Container child) {
if (!(child instanceof Context)) {
throw new IllegalArgumentException
(sm.getString("standardHost.notContext"));
}
child.addLifecycleListener(new MemoryLeakTrackingListener());
// Avoid NPE for case where Context is defined in server.xml with only a
// docBase
Context context = (Context) child;
if (context.getPath() == null) {
ContextName cn = new ContextName(context.getDocBase(), true);
context.setPath(cn.getPath());
}
super.addChild(child);
}
在tomcat设计中,一个应用的所有信息就是一个Context。
在tomcat设计中,Engine既可以包含Host,又可以包含Context。但在Tomcat提供的默认实现StandardEngine中只能包含Host
Context中的addChild,是添加Wrapper:
@Override
public void addChild(Container child) {
// Global JspServlet
Wrapper oldJspServlet = null;
if (!(child instanceof Wrapper)) {
throw new IllegalArgumentException
(sm.getString("standardContext.notWrapper"));
}
......
Wrapper是什么?
public class StandardWrapper extends ContainerBase
implements ServletConfig, Wrapper, NotificationEmitter {
private final Log log = LogFactory.getLog(StandardWrapper.class); // must not be static
protected static final String[] DEFAULT_SERVLET_METHODS = new String[] {
"GET", "HEAD", "POST" };
Wrapper实现了javax.servlet.ServletConfig
,ServletConfig中定义了Servlet的信息,一个ServletConfig对应了一个Servlet,所以在tomcat中,我们可以简单理解Wrapper就是tomcat的Servlet。
那么Container到底应该怎么理解?在上面,容器有时候指Engine、有时候指Host,但是它却代表了一类组件,这类组件的作用就是处理接收客户端的请求并返回响应数据。可能具体操作会委派到子容器完成,但是从行为上,它们是一致的。
注意:既然tomcat的Container可以表示不同的概念级别:Servlet引擎、虚拟主机、web应用和Servlet,那么我们就可以将不同级别的容器作为处理客户端请求的组件,这由我们提供的服务器的复杂度决定。例如我们以嵌入式的方式启动tomcat,且运行极其简单的请求处理,不必支持多web应用的场景,那么我们完全可以只在Service中维护一个简化版的Engine(甚至直接维护一个Context)。
下面简单画出一个类图:
Pipeline和Valve
从架构设计的角度上来看,上面已经完成了对核心概念的分解,确保了整体架构的可伸缩性和可扩展性,除此之外,应当提高每一个组件的灵活性,使其同样易于扩展。
tomcat中采用了责任链模式来实现客户端请求的处理(请求处理也是责任链模式的典型应用场景之一)。换句话说,tomcat中,每个Container组件通过执行一个职责链来完成具体的请求处理。
Tomcat中定义Pipleline(管道)和Valve(阀门)两个接口。前者用于构造职责链,后者代表链上的每个处理器。有点像来自客户端上的请求就像流经管道里的水一样,会经过每个阀进行处理。
设计如下图所示:
Pipeline中维护了一个基础的Valve,定义为basic,它始终位于Pipeline的末端(最后执行),封装了具体的请求处理和输出响应的过程,我们可以为Pipeline添加其他的Valve,后添加的Valve在基础Valve之前,并按照添加顺序执行。Pipeline通过获得首个Valve来启动整个链的顺序执行。具体可参考org.apache.catalina.core.StandardPipeline
类的addValve
方法和setBasic
方法实现。
Tomcat容器灵活之处在于,每个层级的容器(Engine、Host、Context、Wrapper)均有对应的基础Valve实现,同时维护了一个Pipeline实例,所以我们能够在任意层级的容器上对请求处理进行扩展。
下面是简单的类图:
Connector
在tomcat中,有另一个非常重要的组件Connector。想要与Container配合实现一个完整的服务器功能,Connector至少要完成如下几个功能:
- 监听服务器端口,读取来自客户端的请求
- 将请求数据按照指定协议进行解析
- 根据请求地址匹配正确的容器进行处理
- 将响应返回给客户端
tomcat支持多协议,默认支持HTTP和AJP,同时tomcat中还支持多种I/O方式,包括BIO(8.5后被移除)、NIO、APR。tomcat8之后新增了对NIO2和HTTP/2协议的支持。
tomcat的设计方案如下:
在tomcat中,ProtocolHandler
代表协议处理器,针对不同的协议和I/O方式,提供了不同的实现。ProtocolHandler
包含一个Endpoint
用于启动Socket监听,该接口按照I/O方式进行分类实现。(tomcat并没有Endpoint
接口,仅有AbstractEndpoint
抽象类,此处作为概念讨论,将其视为Endpoint
接口)还包含一个Processor用于按照指定协议读取数据,并将请求交由容器处理。
当Processor读取客户端请求后,需要按照请求地址映射到具体的容器进行处理,这个过程称为请求映射。tomcat中各个组件采用通用的生命周期管理,而且通过管理工具进行状态变更。
tomcat通过Mapper
和MapperListener
两个类实现上述功能。前者用于维护容器映射信息,同时按照映射规则(Servlet规范定义)查找容器,后者实现了ContainerListener
和LifecycleListener
,用于在容器组件状态发生变更时,注册或取消对应的容器映射信息。同时MapperListener
继承LifecycleMBeanBase
,间接相当于实现了Lifecycle
接口,当Service
启动时,会自动作为监听器注册到各个组件上,同时将已创建的容器注册到Mapper
。
tomcat通过适配器(org.apache.coyote.Adapter
)模式实现了Connector与Mapper、Container的解耦。tomcat默认的Connector实现对应的适配器为CoyoteAdapter
。
类图如下:
Executor
tomcat提供Executor
接口来表示一个可以在组件间共享的线程池,该接口同样继承自Lifecycle
,可按照通用的组件进行管理。
在tomcat中,Executor由Service维护,因此同一个Service中的组件可以共享一个线程池。如果没有定义任何线程池,相关组件(如Endpoint)会自动创建线程池,此时,线程池不再共享。
在tomcat中,Endpoint会启动一组线程来监听Socket端口,当接收到客户端请求后,会创建请求处理对象,并交由线程池处理,由此支持并发处理客户端请求。
加入线程池后,类图如下:
Boortstrap和Catalina
tomcat通过类Catalina提供了一个Shell程序,用于解析server.xml创建各个组件,同时负责启动、停止应用服务器
tomcat使用Digester解析xml文件,包括server.xml和web.xml。具体可参考org.apache.catalina.startup.Catalina#parseServerXml
。
最后tomcat提供了Bootstrap作为应用服务器的启动入口,Bootstrap负责创建Catalina实例,根据执行参数调用Catalina相关方法完成针对应用服务器的操作。
至此,tomcat应用完整的设计类图如下:
总结
组件名称 | 说明 |
---|---|
Server | 表示整个Servlet容器,因此tomcat运行环境中只有唯一一个Server实例。 |
Service | Service表示一个或者多个Connector的集合,这些Connector共享同一个Container来处理请求。 |
Connector | tomcat连接器,用于监听并转化Socket请求,同时将读取的Socket请求交由Container处理,支持不同协议以及不同的I/O方式。 |
Container | 表示能够执行客户端请求并返回响应的一类对象。 |
Engine | 表示整个Servlet引擎,最为最高层级的容器对象,尽管不是直接处理请求的容器,却是获取目标容器的入口。 |
Host | 表示Servlet引擎中的虚拟机,与一个服务器的网络名有关。 |
Context | 表示ServletContext,在Servlet规范中,一个ServletContext表示一个独立的web应用。 |
Wrapper | 表示web应用中定义的Servlet。 |
Executor | 组件中可以共享的线程池。 |