通过之前的 Apache Tomcat 可以把得到如下的 Tomcat 的架构图:
1、请求处理核心组件
以下是 Tomcat 处理请求时涉及到的核心组件:
组件名称 | 说明 |
---|---|
EndPoint | Endpoint 可以理解成通信端点,用于 Socket 通信处理 TCP/IP 协议。监听 Socket 暴露的端口,当有请求进来的时候 EndPoint 进行处理。支持 BIO、NIO、NIO2 及 APR 网络 IO。 |
Processor | 处理器组件,解析处理 HTTP 报文。当 EndPoint 监听到网络请求时,把网络请求交给 Processor 解析 HTTP 协议。把 HTTP 请求解析成 Request 与 Response 对象 |
ProtocolHandler | 这个组件持有 EndPoint 组件与 Processor 组件 |
Adapter | 适配器组件,把 Processor 解析后的 Request 对象适配成 Servlet 容器中需要使用的 ServletRequest 以及把 Servlet 容器中的 ServletResponse 适配成响应的 Response 对象 |
Engine | Engine 表示整个 Servlet 引擎。在 Tomcat 中,Engine 为最高层级的容器对象。尽管 Engine 不是直接处理请求的容器,却是获取目标容器的入口 |
Host | Host 作为一类容器,表示 Servlet 引擎(即 Engine) 中的虚拟机,与一个服务器的网络名有关,如域名等。客户端可以使用这个网络名连接服务器,这个名称必须要在 DNS 服务器上注册 |
Context | Context 作为一类容器,用于表示 ServletContext,在 Servlet 规范中,一个 ServletContext 即表示一个独立的 Web 应用 |
Wrapper | Wrapper 作为一类容器,用于表示 Web 应用中定义的 Servlet |
Mapper | 维护 Host、Context 和 Wrapper 容器的映射,同时通过 MapperListener 监听器监听所有的 Host、Context、 Wrapper 组件,在相关组件启动、停止或者移除相关映射 |
2、定义一个 Servlet
为了进行代码 debug 方便,我们需要创建一个简单的 Web 项目,并且定义一个 Servlet。并且满足 Tomcat Web 规范。下面就是项目的结构图:
在 webapps 下面定义一个我们的 Web 项目,项目名称叫 web-demo,并且按照 Tomcat 的 web 规范定义一个 WEB-INF,最主要包含 2 个东西:
- classes : 定义业务处理的 Servlet
- web.xml :把 Servlet 规范信息,包含 Listener、Filter、Servlet 等声明
定义一个简单的 Servlet 声明类:
HelloWorldServlet
public class HelloWorldServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("invoke HelloWorldServlet.doGet success");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("invoke HelloWorldServlet.doPost success");
}
}
把这个 Servlet 声明到 web.xml 文件里面:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1"
metadata-complete="true">
<description>
Servlet and JSP Examples.
</description>
<display-name>Servlet and JSP Examples</display-name>
<servlet>
<servlet-name>HelloWorldServlet</servlet-name>
<servlet-class>cn.carlzone.tomcat.servlet.HelloWorldServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>HelloWorldServlet</servlet-name>
<url-pattern>/servlets/hello</url-pattern>
</servlet-mapping>
</web-app>
下面就开启 Apache Tomcat 请求处理,源码分析。
3、Servlet 请求处理
要明白 Tomcat 请求处理是如何处理的,我们首先需要想明白一个问题。
当一个 Servlet 请求到来的时候,Tomcat 是通过怎样的机制锁定到 Servlet 并且执行的。
当我们需要访问上面定义的 HelloWorldServlet 的时候,是不是需要在浏览器里面输入 http://localhost:8080/web-demo/servlets/hello
。根据之前对 Tomcat 里面的容器组件分析我们可以得到下面架构图:
当访问地址:http://localhost:8080/web-demo/servlets/hello
的时候, Connector 组件会接收并按照协议解析 http 请求,然后把解析好的请求发送给 Tomcat 里面的容器组件 (Container)。通过责任链的形式交给 Engine、Host、Context 、Wrapper 来处理。在之前的文章中我们说到 Tomcat 是通过 Mapper 和 MapperListener 来维护请求映射的。在之前的启动图里面没有 Mapper 和 MapperListener 的时序图。在这里我们就来分析一下 Tomcat 是如何构建请求路径映射的。
4、Mapper 和 MapperListener
在之前的 Tomcat 的架构演进之中分析了 Tomcat 是通过 Service 这个组件来管理请求与 Http 请求的。Service 持有 Mapper 和 MappingListener 这两个组件并且利用它们来做请求映射。下面我们就来分析一下 Mapper 和 MapperListener 这两个组件。首先来看一下 Mapper,下面就是它的类图:
它们的关系是:
- 一个 Mapper 对象里面持有多个 MappedHost
- 一个 MappedHost 里面持有 多个MappedContext
- 一个 MapperContext 里面持有多个 MappedWrapper
- MappedXxx 对象都继承自 MapElement 对象
- MapMapElement 对象包含了名称和和对象两个属性。名称的作用就是 http 请求路径的 key,Object 就是对应的处理容器。比如:
http://localhost:8080/web-demo/hello
,MappedHost 对应的key 就是 localhost,对应的处理器就是StandardHost
;MappedContext 对应的 key 就是 web-demo ,对应的处理容器就是 StandardContext;MappedWrapper 对应的 key 就是 hello,对应的处理容器就是 StandardWrapper。
那么这个映射关系是什么时候构建的呢?在 Service 刚刚创建的时候只是通过 new 关键字来创建这个对象。其实是通过 MapperListener 来构建 Mapper 对象里面的映射关系的。MapperListener 其实也是实现了生命周期管理组件 LifecycleListener 。在 Tomcat 启动时,它随着 Service 的生命周期而被调用。
StandardService#startInternal
protected void startInternal() throws LifecycleException {
if(log.isInfoEnabled())
log.info(sm.getString("standardService.start.name", this.name));
setState(LifecycleState.STARTING);
// Start our defined Container first
if (engine != null) {
synchronized (engine) {
engine.start();
}
}
synchronized (executors) {
for (Executor executor: executors) {
executor.start();
}
}
mapperListener.start();
// Start our defined Connectors second
synchronized (connectorsLock) {
for (Connector connector: connectors) {
try {
// If it has already failed, don't try and start it
if (connector.getState() != LifecycleState.FAILED) {
connector.start();
}
} catch (Exception e) {
log.error(sm.getString(
"standardService.connector.startFailed",
connector), e);
}
}
}
}
它的时机是在 service 服务调用 start 方法,也就是当前服务的容器都初始化完成之后,Connector 服务调用 start 方法之前。在这个时候,当前的容器父子级关系已经解析完成了。
- 当前服务包含哪些虚拟主机(一对多)
- 当前虚拟主机下包含哪些 Context(一对多)一个 tomcat 下可以发布多个 War 包。
- 当前 Context 下包含哪些 Wrapper(一对多)也就是 web.xml 里面定义的 Servlet。
随着 Tomcat 的生命周期构建起 http 请求与 Servlet 请求的映射关系:
5、CoyoteAdapter
org.apache.catalina.connector.CoyoteAdapter
的作用是把 Connector 与 Mapper、Containner 联系起来。当 Connector 接收到请求后,首先读取数据,然后调用 CoyoteAdapter#service 方法完成请求处理。它的主要处理过程包含以下几个步骤:
- 根据 Connector 的请求(org.apache.coyote.Request) 和响应(org.apache.coyote.Response)对象创建 Servlet 请求(org.apache.catalina.connector.Request) 和响应(org.apache.catalina.connector.Response)
- 转换请求参数并通过 MappingData 与请求映射 URI 定位到具体处理的 Wrapper
- 得到当前 Engine 的第一个 Valve 并执行(invoke),完成客户端请求处理
请求与映射关系的查询时序图如下:
6、Pipeline 与 Valve
Tomcat 为了方便扩展采用了 Pipeline 与 Valve 构建成责任链来调用。
它的创建时机是在每个容器的初始化的时候:
StandardEngine
public StandardEngine() {
super();
pipeline.setBasic(new StandardEngineValve());
/* Set the jmvRoute using the system property jvmRoute */
try {
setJvmRoute(System.getProperty("jvmRoute"));
} catch(Exception ex) {
log.warn(sm.getString("standardEngine.jvmRouteFail"));
}
// By default, the engine will hold the reloading thread
backgroundProcessorDelay = 10;
}
StandardHost
public StandardHost() {
super();
pipeline.setBasic(new StandardHostValve());
}
StandardContext
public StandardContext() {
super();
pipeline.setBasic(new StandardContextValve());
broadcaster = new NotificationBroadcasterSupport();
// Set defaults
if (!Globals.STRICT_SERVLET_COMPLIANCE) {
// Strict servlet compliance requires all extension mapped servlets
// to be checked against welcome files
resourceOnlyServlets.add("jsp");
}
}
StandardWrapper
public StandardWrapper() {
super();
swValve=new StandardWrapperValve();
pipeline.setBasic(swValve);
broadcaster = new NotificationBroadcasterSupport();
}
容器在初始化的时候设置当前容器的 Pipeline 的 Basic 属性为当前容器对应的 Valve,然后在容器中以下面方法进行调用
container.getPipeline().getFirst().invoke(request, response)
其实就是调用初始化方法中设置的在当前容器 Pipeline 中对应的 Valve。
public Valve getFirst() {
if (first != null) {
return first;
}
return basic;
}
好了,到此整个 Tomcat 的请求处理都分析完了。