通过之前两篇关于Tomcat的介绍,我们已经清楚知道Tomcat的作用及基本的工作原理。本篇文章开始,我们来从解读Tomcat源码。本篇先来介绍一下Tomcat的基础类,以便我们后续介绍Tomcat启动流程,工作流程。
我们知道,Tomcat的能作为web容器正常工作,依赖于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" />
<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>
上篇文章我们也讲过,上述xml配置节点,基本都是Tomcat的基础组件。比如Server、Service、Connector、Container、Engine、Host、Context等。也在上篇文章大致介绍了上述组件的基本功能。我们今天要介绍的Tomcat基础类,就是Tomcat源码中对上述组件的抽象和实现。
1. Server
Server 组件的具体实现类是 StandardServer。它的子组件是Service,因此它还需要管理Service的生命周期,也就是说在启动时调用Service组件的启动方法,在停止时调用Service组件的停止方法。Server在内部维护了若干 Service 组件,它是以数组来保存的。
StandardServer内部提供了一个addService方法,用于将Service子组件添加到数组services[]中,如下:
public void addService(Service service) {
service.setServer(this);
synchronized (servicesLock) {
Service results[] = new Service[services.length + 1];
System.arraycopy(services, 0, results, 0, services.length);
results[services.length] = service;
services = results;
if (getState().isAvailable()) {
try {
service.start();
} catch (LifecycleException e) {
// Ignore
}
}
// Report this property change to interested listeners
support.firePropertyChange("service", null, service);
}
}
它并没有一开始就分配一个很长的数组,而是在添加的过程中动态地扩展数组长度,当添加一个新的 Service 实例时,会创建一个新数组并把原来数组内容复制到新数组。
除了Service子组件的维护,Server另一个重要的功能就是来实现Tomcat的关闭,这也是为什么你可以通过shutdown命令来关闭Tomcat的原因。具体实现在StandardServer的await方法中,在await方法中会启动一个Socket来监听8005(停止端口),并在一个死循环里接收Socket上的连接请求,如果有新的连接到来就建立连接,然后从Socket中读取数据,如果读到的数据是停止命令“SHUTDOWN”,就退出循环,进入stop流程。
public void await() {
// Negative values - don't wait on port - tomcat is embedded or we just don't like ports
if (getPortWithOffset() == -2) {
// undocumented yet - for embedding apps that are around, alive.
return;
}
if (getPortWithOffset() == -1) {
try {
awaitThread = Thread.currentThread();
while(!stopAwait) {
try {
Thread.sleep( 10000 );
} catch( InterruptedException ex ) {
// continue and check the flag
}
}
} finally {
awaitThread = null;
}
return;
}
// Set up a server socket to wait on
try {
// 创建socket,默认端口8005
awaitSocket = new ServerSocket(getPortWithOffset(), 1,
InetAddress.getByName(address));
} catch (IOException e) {
// log ……
return;
}
try {
awaitThread = Thread.currentThread();
// 死循环,等待停止命令
while (!stopAwait) {
ServerSocket serverSocket = awaitSocket;
if (serverSocket == null) {
break;
}
// 等待连接,从连接中获取请求命令
Socket socket = null;
StringBuilder command = new StringBuilder();
try {
InputStream stream;
long acceptStartTime = System.currentTimeMillis();
try {
socket = serverSocket.accept();
socket.setSoTimeout(10 * 1000); // Ten seconds
stream = socket.getInputStream();
} catch (SocketTimeoutException ste) {
// log ……
continue;
} catch (AccessControlException ace) {
// log ……
continue;
} catch (IOException e) {
if (stopAwait) {
// Wait was aborted with socket.close()
break;
}
// log ……
break;
}
// Read a set of characters from the socket
int expected = 1024; // Cut off to avoid DoS attack
while (expected < shutdown.length()) {
if (random == null)
random = new Random();
expected += (random.nextInt() % 1024);
}
while (expected > 0) {
int ch = -1;
try {
ch = stream.read();
} catch (IOException e) {
log.warn(sm.getString("standardServer.accept.readError"), e);
ch = -1;
}
// Control character or EOF (-1) terminates loop
if (ch < 32 || ch == 127) {
break;
}
command.append((char) ch);
expected--;
}
} finally {
// Close the socket now that we are done with it
try {
if (socket != null) {
socket.close();
}
} catch (IOException e) {
// Ignore
}
}
// 如果连接是SHUTDOWN命令,结束循环
boolean match = command.toString().equals(shutdown);
if (match) {
log.info(sm.getString("standardServer.shutdownViaPort"));
break;
} else
log.warn(sm.getString("standardServer.invalidShutdownCommand", command.toString()));
}
} finally {
ServerSocket serverSocket = awaitSocket;
awaitThread = null;
awaitSocket = null;
// Close the server socket and return
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
// Ignore
}
}
}
}
2. Service
Service组件的具体实现类是StandardService。通过之前的文章我们知道,Service组件中有一个或多个Connector和一个Container。所以Service需要负责管理Connector和Container的生命周期。
除此之外,Service内部还有一个比较重要的成员就是映射器Mapper及其监听器,映射器及其监听器用于将用户请求的 URL 定位到一个 Servlet。它的工作原理是: Mapper组件里保存了Web应用的配置信息,其实就是容器组件与访问路径的映射关系,比如Host容器里配置的域名、Context容器里的Web应用路径,以及 Wrapper容器里Servlet映射的路径。映射器Mapper的监听器用于支持热部署,当Web应用的部署发生变化时,Mapper中的映射信息也要跟着变化,MapperListener就是一个监听器,它监听容器的变化,并把信息更新到Mapper 中,这是典型的观察者模式。
Service组件最重要的功能对下层容器的管理,比如启动时,要负责启动Connector和Container组件。Service的启动方法在startIntel中,如下:
protected void startInternal() throws LifecycleException {
if(log.isInfoEnabled())
log.info(sm.getString("standardService.start.name", this.name));
// 1. 触发启动监听器
setState(LifecycleState.STARTING);
// 2. 先启动Engine,Engine会启动它子容器
if (engine != null) {
synchronized (engine) {
engine.start();
}
}
synchronized (executors) {
for (Executor executor: executors) {
executor.start();
}
}
// 3. 再启动Mapper监听器
mapperListener.start();
// 4. 最后启动连接器,连接器会启动它子组件,比如Endpoint
synchronized (connectorsLock) {
for (Connector connector: connectors) {
// If it has already failed, don't try and start it
if (connector.getState() != LifecycleState.FAILED) {
connector.start();
}
}
}
}
从启动方法可以看到,Service先启动了Engine组件,再启动Mapper监听器,最后才是启动连接器。这很好理解,因为内层组件启动好了才能对外提供服务,才能启动外层的连接器组件。而 Mapper 也依赖容器组件,容器组件启动好了才能监听它们的变化,因此Mapper和MapperListener在容器组件之后启动。组件停止的顺序跟启动顺序正好相反的,也是基于它们的依赖关系。
3. Connector
通过之前的文章我们知道,连接器的作用是处理 Socket 连接,将网络字节流转化为Request和Response对象。也就是说连接器对Servlet容器屏蔽了协议及I/O模型等的区别,无论是HTTP还是AJP,对容器而言,获取到的都是一个标准的ServletRequest对象。连接器的工作流程可以细分为如下几个部分:
- 监听网络端口
- 接受网络连接请求
- 读取请求网络字节流
- 根据具体应用层协议(HTTP/AJP)解析字节流,生成统一的 Tomcat Request 对象
- 将 Tomcat Request 对象转成标准的 ServletRequest
- 调用 Servlet 容器,得到 ServletResponse
- 将 ServletResponse 转成 Tomcat Response 对象
- 将 Tomcat Response 转成网络字节流
- 将响应字节流写回给浏览器
Tomcat设计者将上述功能进一步细分为以下三部分:
- 网络通信
- 应用层协议解析
- Tomcat Request/Response 与 ServletRequest/ServletResponse 的转化
Tomcat 的设计者设计了3个组件来实现这3个功能,分别是EndPoint、Processor和Adapter。
网络通信的I/O模型是变化的,可能是非阻塞 I/O、异步 I/O 或者 APR。应用层协议也是变化的,可能是 HTTP、HTTPS、AJP。浏览器端发送的请求信息也是变化的。但是整体的处理逻辑是不变的,EndPoint负责提供字节流给 Processor,Processor负责提供Tomcat Request对象给Adapter,Adapter 负责提供 ServletRequest对象给容器。
如果要支持新的I/O方案、新的应用层协议,只需要实现相关的具体子类。上层通用的处理逻辑是不变的。
由于I/O模型和应用层协议可以自由组合,比如NIO + HTTP或者NIO2 + AJP。Tomcat的设计者将网络通信和应用层协议解析放在一起考虑,设计了一个叫 ProtocolHandler的接口来封装这两点变化点。各种协议和通信模型的组合有相应的具体实现类,比如: Http11NioProtocol 和 AjpNioProtocol。
除了这些变化点,系统也存在一些相对稳定的部分,因此 Tomcat 设计了一系列抽象基类来封装这些稳定的部分,抽象基类AbstractProtocol实现了 ProtocolHandler接口。每一种应用层协议有自己的抽象基类,比如 AbstractAjpProtocol和AbstractHttp11Protocol,具体协议的实现类扩展了协议层抽象基类。
这样设计的目的是尽量将稳定的部分放到抽象基类,同时每一种 I/O 模型和协议的组合都有相应的具体实现类,我们在使用时可以自由选择。所以Tomcat连接器可以细分为如下几个部分:
3.1 ProtocolHandler
ProtocolHandler成员变量初始化是在Connector构造函数中完成的,如果调用Connector无参构造函数,ProtocolHandler默认为HTTP/1.1 NIO类型,即Http11NioProtocol。这里我们也以Http11NioProtocol为例,介绍ProtocolHandler。
ProtocolHandler用来处理网络连接和应用层协议,包含了2个重要部件:EndPoint和Processor
3.1.1 Endpoint
EndPoint是通信端点,即通信监听的接口,是具体的Socket接收和发送处理器,是对传输层的抽象,因此EndPoint是用来实现TCP/IP协议的。
EndPoint是一个接口,对应的抽象实现类是AbstractEndpoint,而 AbstractEndpoint的具体子类,比如在 NioEndpoint和Nio2Endpoint中,有两个重要的子组件:Acceptor和SocketProcessor。
其中Acceptor用于监听Socket连接请求。SocketProcessor用于处理接收到的 Socket请求,它实现Runnable接口,在run()方法里调用协议处理组件Processor 进行处理。为了提高处理能力,SocketProcessor被提交到线程池来执行。
3.1.2 Processor
EndPoint用来实现TCP/IP协议,Processor则用来实现应用层协议的(HTTP协议、AJP协议等),负责接收来自EndPoint的Socket,读取字节流解析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理。
Processor是一个接口,定义了请求的处理等方法。它的抽象实现类 AbstractProcessor对一些协议共有的属性进行封装,没有对方法进行实现。具体的实现有AJPProcessor、HTTP11Processor等,这些具体实现类实现了特定协议的解析方法和请求处理方式。
上图的流程代码逻辑大致如下:
NioEndPoint.startIntel ->
Poller -> run()
-> processKey(SelectionKey sk, NioSocketWrapper attachment)
-> processSocket(attachment, SocketEvent.OPEN_READ, true)
-> createSocketProcessor(socketWrapper, event)
-> SocketProcessor run
-> getHandler().process
-> processor.process(wrapper, status)
-> service(socketWrapper)
-> getAdapter().service(request, response)
3.2 Adapter
由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了自己的 Request类来存放这些请求信息。ProtocolHandler接口负责解析请求并生成Tomcat Request类。但是这个Request对象不是标准的ServletRequest,也就意味着,不能用Tomcat Request作为参数来调用容器。Tomcat设计者的解决方案是引入CoyoteAdapter,这是适配器模式的经典运用,连接器调用CoyoteAdapter 的 Sevice方法,传入的是Tomcat Request对象,CoyoteAdapter负责将Tomcat Request转成ServletRequest,再调用Engine容器的pipline方法(之前文章介绍的pipline-valve机制),实现对servlet的调用。
4. Engine
Engine是一个容器(或者讲Engine是Tomcat的顶级容器,Tomcat设计了4 种容器,分别是 Engine、Host、Context 和 Wrapper),因此它继承了ContainerBase基类,并且实现了Engine接口,具体实现类是StandardEngine。
Engine的子容器是Host,负责子容器Host的启动暂停。所以它持有了一个Host容器的数组,这些功能都被抽象到了ContainerBase中,ContainerBase中有这样一个数据结构:
protected final HashMap<String, Container> children = new HashMap<>();
ContainerBase用HashMap保存了它的子容器,并且ContainerBase还实现了子容器的“增删改查”,甚至连子组件的启动和停止都提供了默认实现,比如 ContainerBase会用专门的线程池来启动子容器。
Engine在启动Host子容器时就直接重用了这个方法(startIntel)。在StandardEngine中,我们只看到了容器启动、暂停相关的方法。我们知道,Engine作为容器,最重要的肯定是处理请求了,那么Engine是如何处理请求的?上面介绍Adapter的时候我们知道,Adapter中会调用Engine的pipline的invoke方法,如下:
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
从而实现将请求提交给容器处理,可以看到调用的就是Engine的Pipeline。之前介绍Pipline-valve机制的时候,我们介绍过,每一个容器组件都有一个Pipeline,而 Pipeline中有一个基础阀(Basic Valve),基础阀负责调用下层容器Pipeline。Engine容器的基础阀StandardEngineValve定义如下:
final class StandardEngineValve extends ValveBase {
//------------------------------------------------------ Constructor
public StandardEngineValve() {
super(true);
}
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// 拿到请求中的host容器
Host host = request.getHost();
if (host == null) {
// HTTP 0.9 or HTTP 1.0 request without a host when no default host
// is defined. This is handled by the CoyoteAdapter.
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
}
// 调用host容器pipline中第一个valve,将请求提交给host处理
host.getPipeline().getFirst().invoke(request, response);
}
}
可以看到,Engine对请求的处理,其实就是把请求转发给某一个 Host 子容器来处理。
5. Host
Host是Tomcat顶层容器Engine的子容器,通过我们之前web.xml配置不难发现,Host是跟虚拟域名绑定的,同时负责维护其子容器Context的生命周期。Host容器在Tomcat中的实现类是StandardHost,它也继承了ContainerBase基类同时实现了Host接口。
public class StandardHost extends ContainerBase implements Host{
//
}
因为继承了ContainerBase类,所以对于Context子容器的维护,也是基于ContainerBase的统一结构(上面介绍Engine的时候介绍过,HashMap children)。在StandardHost中也没有重写startInternal方法,所以StandardHost的启动功能也很明确——用于启动子容器Context。
6. Context
Context对应一个web应用,是用于存储管理Servlet的容器,在Tomcat中的默认实现是StandardContext。StandardContext也继承了ContainerBase类,子容器也是通过ContainerBase类的成员变量children维护的,但是StandardContext中重写了startInternal方法,在该方法中,完成WebAppClassLoader的初始化,子容器Wrapper的初始化及启动,web.xml配置文件解析,Context监听器维护与触发等。
6.1 基础属性
比如我们在配置文件中配置的Context的path属性,用于指定Host下(域名下)web应用的访问路径:
<Host appBase="webapps" autoDeploy="true" name="www.lidol.top" unpackWARs="true">
<!--访问路径:www.lidol.top/demo1-->
<Context docBase="/Users/zhuoli/Documents/demo/demo1" path="/demo1" reloadable="true"/>
<!--访问路径:www.lidol.top/demo2-->
<Context docBase="/Users/zhuoli/Documents/demo/demo2" path="/demo2" reloadable="true"/>
</Host>
在StandardContext中,我们发现存在这样两个成员变量:
/**
* The document root for this web application.
*/
private String docBase = null;
/**
* Unencoded path for this web application.
*/
private String path = null;
Context是所有的Servlet的父容器,我们知道Servlet中一个重要的组件就是ServletContext(Servlet上下文),该组件就是在StandardContext中初始化的:
/**
* The ServletContext implementation associated with this Context.
*/
protected ApplicationContext context = null;
@Override
public ServletContext getServletContext() {
if (context == null) {
context = new ApplicationContext(this);
if (altDDName != null)
context.setAttribute(Globals.ALT_DD_ATTR,altDDName);
}
return (context.getFacade());
}
6.2 Context和Java Web三大组件
我们之前介绍过Servlet三大组件,分别是Servlet、Filter和Listener。这三个组件的作用于都是整个Web应用。
Servlet其实就是Context的子容器Wrapper,用继承的children成员变量来维护。
Filter是一种Servlet的扩展机制,在web.xml中配置,在Context容器中进行管理的:
private Map<String, FilterDef> filterDefs = new HashMap<>();
需要注意的是StandardContext中只是将我们配置的Filter解析成FilterDef,并不是每个请求都需要将所有的Filter走一遍。在web.xml中,我们通过<filter-mapping>来配置servlet生效的Filter集合,Tomcat容器会为每个请求生成一个FilterChain,用于表示该请求相关联的Filter,在每个Filter执行结束后,才会调用Servlet的service方法。
Listener跟Filter一样,也是一种扩展机制,你可以监听容器内部发生的事件,主要有两类事件:
- 生命状态的变化,比如Context容器启动和停止、Session的创建和销毁
- 属性的变化,比如Context容器某个属性值变了、Session的某个属性值变了以及新的请求来了等
我们在web.xml配置了监听器,在监听器里实现我们的业务逻辑。对于Tomcat来说,它需要读取配置文件,拿到监听器类的名字,实例化这些类,并且在合适的时机调用这些监听器的方法。
Tomcat是通过Context容器来管理这些监听器的。Context容器将两类事件分开来管理,分别用不同的集合来存放不同类型事件的监听器:
// 监听属性值变化的监听器
private List<Object> applicationEventListenersList = new CopyOnWriteArrayList<>();
// 监听生命事件的监听器
private Object applicationLifecycleListenersObjects[] = new Object[0];
剩下的事情就是触发监听器了,比如在Context容器的启动方法里,就触发了所有的ServletContextListener:
for (int i = 0; i < instances.length; i++) {
if (!(instances[i] instanceof ServletContextListener))
continue;
ServletContextListener listener =
(ServletContextListener) instances[i];
try {
fireContainerEvent("beforeContextInitialized", listener);
if (noPluggabilityListeners.contains(listener)) {
listener.contextInitialized(tldEvent);
} else {
listener.contextInitialized(event);
}
fireContainerEvent("afterContextInitialized", listener);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
fireContainerEvent("afterContextInitialized", listener);
getLogger().error
(sm.getString("standardContext.listenerStart",
instances[i].getClass().getName()), t);
ok = false;
}
}
7. Wrapper
Wrapper也是一种容器,用来管理Servlet,每一个Servlet都对应一个Wrapper,在Tomcat中的实现是StandardWrapper。所以StandardWrapper中有一个比较重要的成员变量:
protected volatile Servlet instance = null;
用于表示该Wrapper容器对应的Servlet实例。在StandardWrapper中提供了allocate()方法,用于实例化并初始化Servlet。而allocate方法最终调用了StandardWrapper中的loadServlet()方法来实例化并初始化Servlet。
public synchronized Servlet loadServlet() throws ServletException {
// Nothing to do if we already have an instance or an instance pool
if (!singleThreadModel && (instance != null))
return instance;
// 1. 创建Servlet实例
servlet = (Servlet) instanceManager.newInstance(servletClass);
// 2. 调用Servlet的init方法,初始化Servlet
initServlet(servlet);
return servlet;
}
loadServlet()方法并不会在Tomcat启动的时候调用,为了加快启动速度,Tomcat采取了资源延迟加载的策略。默认情况下Tomcat在启动时不会加载你的 Servlet,除非把Servlet的loadOnStartup属性设置为true。但是在Tomcat容器启动时,是会创建Wrapper容器的。
那么loadServlet()方法到底是什么时候调用的?答案是StandardWrapperValve中。上面我们介绍到,连接器的Adapter组件会把请求提交给容器Engine的Pipline,之后通过Pipline-Valve机制,会调用到StandardWrapperValve的invoke方法,就是在该方法中,完成Servlet的初始化和实例化。如下:
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// 1. 实例化Servlet
servlet = wrapper.allocate();
// 2. 为当前请求创建一个FilterChain
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
// 3. 调用这个FilterChain的doFilter方法,FilterChain中最后一个Filter执行结束后,会调用Servlet的service方法
filterChain.doFilter(request.getRequest(), response.getResponse());
}
到这里,我们也可以解答一个疑问,为什么Servlet会在第一次被调用时实例化。因为在上述loadServlet方法中,只有Wrapper容器中的Servlet实例为null时(第一次调用时),才会创建Servlet实例。
参考链接:
1. Tomcat源码
2. 深入了解Tomcat&Jetty