二. Tomcat 从启动流程到请求处理
说明: 使用的源码版本是tomcat 8.5
1. Tomcat 启动流程
1. Tomcat 源码目录
catalina 目录
catalina 包含所有的 Servlet 容器实现,以及涉及到安全、会话、集群、部署管理 Servlet 容器的各个方面,同时,它还包含了启动入口。
coyote 目录
coyote 是 Tomcat 链接器框架的名称,是 Tomcat 服务器提供的客户端访问的外部接口,客户端通过 Coyote 与服务器建立链接、发送请求并接收响应。
EL 目录,提供 java 表达式语言
Jasper 模块提供 JSP 引擎
Naming 模块提供 JNDI 的服务
Jul 提供服务器日志的服务
tomcat 提供外部调用的接口 api
2. Tomcat 启动流程分析
启动流程解析:注意是标准的启动,也就是从 bin 目录下的启动文件中启动 Tomcat(Spring Boot启动方式通过OnRefresh方法实例化嵌入式的Tomcat)

我们可以看到这个流程非常的清晰,同时注意到,Tomcat 的启动非常的标准,除去 Boostrap 和 Catalin,我们可以对照一下 Server.xml 的配置文件。Server,service 等等这些组件都是一一对照,同时又有先后顺序。
基本的顺序是先 init 方法,然后再 start 方法。
**加入调试信息:**注意是标准的启动,也就是从bin 目录下的启动文件中启动 Tomcat
在适当的位置打印对象信息, 便于分析流程, 添加打印的位置
org.apache.catalina.startup.Bootstrap#load
private void load(String[] arguments)
throws Exception {
System.out.println("Bootsrap--load()"); // 这里
// Call the load() method
String methodName = "load";
Object param[];
Class<?> paramTypes[];
if (arguments == null || arguments.length == 0) {
paramTypes = null;
param = null;
} else {
paramTypes = new Class[1];
paramTypes[0] = arguments.getClass();
param = new Object[1];
param[0] = arguments;
}
Method method =
catalinaDaemon.getClass().getMethod(methodName, paramTypes);
if (log.isDebugEnabled())
log.debug("Calling startup class " + method);
method.invoke(catalinaDaemon, param);
}
org.apache.catalina.startup.Catalina#load()
/**
* 启动新的服务器实例。
*/
public void load() {
System.out.println("Catalina--load()"); // 这里
if (loaded) {
return;
}
...
}
org.apache.catalina.startup.Catalina#load(java.lang.String[])
public void load(String args[]) {
System.out.println("Catalina--load()"); // 这里
try {
if (arguments(args)) {
load();
}
} catch (Exception e) {
e.printStackTrace(System.out);
}
}
除了 Bootstrap 和 catalina 类,其他的 Server,service 等等之类的都只是一个接口,实现类均为 StandardXXX 类。 我们来看下 StandardServer 类,问题来了,我们发现 StandardServer 类中没有 init 方法,只有一个类似于 init 的 initInternal 方法,这个是为什么? 带着问题我们进入下面的内容。
2. 分析 Tomcat 请求过程
解耦:网络协议与容器的解耦。
Connector 链接器封装了底层的网络请求(Socket 请求及相应处理),提供了统一的接口,使 Container 容器与具体的请求协议以及 I/O 方式解耦。
Connector 将 Socket 输入转换成 Request 对象,交给 Container 容器进行处理,处理请求后,Container 通过 Connector 提供的 Response 对象将结果写入输出流。
因为无论是 Request 对象还是 Response 对象都没有实现 Servlet 规范对应的接口,Container 会将它们进一步分装成 ServletRequest 和 ServletResponse.
org.apache.coyote.Request: 连接器使用的Request
org.apache.catalina.connector.Request: 符合servlet规范, 有输入流, Response类似
问题来了,在 Engine 容器中,有四个级别的容器,他们的标准实现分别是 StandardEngine、StandardHost、StandardContext、StandardWrapper。
3. 组件的生命周期管理
1. 各种组件如何统一管理
Tomcat 的架构设计是清晰的、模块化、它拥有很多组件,加入在启动 Tomcat 时一个一个组件启动,很容易遗漏组件,同时还会对后面的动态组件拓 展带来麻烦。如果采用我们传统的方式的话,组件在启动过程中如果发生异常,会很难管理,比如你的下一个组件调用了 start 方法,但是如果它的上级 组件还没有 start 甚至还没有 init 的话,Tomcat 的启动会非常难管理,因此,Tomcat 的设计者提出一个解决方案:用 Lifecycle 管理启动,停止、关闭。
2. 生命周期统一接口–Lifecycle
Tomcat 内部架构中各个核心组件有包含与被包含关系,例如:Server 包含了 Service.Service 又包含了 Container 和 Connector,这个结构有一点像数据结 构中的树,树的根结点没有父节点,其他节点有且仅有一个父节点,每一个父节点有 0 至多个子节点。所以,我们可以通过父容器启动它的子容器,这 样只要启动根容器,就可以把其他所有的容器都启动,从而达到了统一的启动,停止、关闭的效果。
4. 分析 Tomcat 请求过程
1. Host 设计的目的
Tomcat 诞生时,服务器资源很贵,所以一般一台服务器其实可以有多个域名映射,满了满足这种需求,比如,我的这台电脑,有一个 localhost 域名, 同时在我的 hosts 文件中配置两个域名,一个 www.a.com 一个 localhost。
2. Context 设计的目的
container 从上一个组件 connector 手上接过解析好的内部 request,根据 request 来进行一系列的逻辑操作,直到调用到请求的 servlet,然后组装好 response, 返回给 connecotr
先来看看 container 的分类吧:
Engine
Host
Context
Wrapper
它们各自的实现类分别是 StandardEngine, StandardHost, StandardContext,StandardWrapper,他们都在 tomcat 的 org.apache.catalina.core 包下。
它们之间的关系,可以查看 tomcat 的 server.xml 也能明白(根据节点父子关系),这么比喻吧:除了 Wrapper 最小,不能包含其他 container 外,Context内可以有零或多个 Wrapper,Host 可以拥有零或多个 Context ,Engine 可以有零到多个 Host。
Standard 的 container 都是直接继承抽象类:org.apache.catalina.core.ContainerBase:
public abstract class ContainerBase extends LifecycleMBeanBase implements Container {
3. Tomcat 处理一个 HTTP 请求的过程
用户点击网页内容,请求被发送到本机端口 8080,被在那里监听的 Coyote HTTP/1.1 Connector 获得。
Connector 把该请求交给它所在的 Service 的 Engine 来处理,并等待 Engine 的回应。
Engine 获得请求 localhost/test/index.jsp,匹配所有的虚拟主机 Host。
Engine 匹配到名为 localhost 的 Host(即使匹配不到也把请求交给该 Host 处理,因为该 Host 被定义为该 Engine 的默认主机),名为 localhost 的 Host 获得请求/test/index.jsp,匹配它所拥有的所有的 Context。Host 匹配到路径为/test 的 Context(如果匹配不到就把该请求交给路径名为“”的 Context 去处理)。 path=“/test”的 Context 获得请求/index.jsp,在它的 mapping table 中寻找出对应的 Servlet。Context 匹配到 URL PATTERN 为*.jsp 的 Servlet,对应于 JspServlet类。
构造 HttpServletRequest 对象和 HttpServletResponse 对象,作为参数调用 JspServlet 的 doGet()或 doPost().执行业务逻辑、数据存储等程序。 Context 把执行完之后的 HttpServletResponse 对象返回给 Host。
Host 把 HttpServletResponse 对象返回给 Engine。
Engine 把 HttpServletResponse 对象返回 Connector。
Connector 把 HttpServletResponse 对象返回给客户 Browser。
5. 管道模式
1. 管道与阀门
在一个比较复杂的大型系统中,如果一个对象或数据流需要进行繁杂的逻辑处理,我们可以选择在一个大的组件中直接处理这些繁杂的逻辑处理, 这个方式虽然达到目的,但是拓展性和可重用性差。因为牵一发而动全身。
管道是就像一条管道把多个对象连接起来,整体看起来就像若干个阀门嵌套在管道中,而处理逻辑放在阀门上。
它的结构和实现是非常值得我们学习和借鉴的。
每一种 container 都有一个自己的 StandardValve
StandardEngineValve
StandardHostValve
StandardContextValve
StandardWrapperValve
Pipeline 就像一个工厂中的生产线,负责调配工人(valve)的位置,valve 则是生产线上负责不同操作的工人。 一个生产线的完成需要两步:
1,把原料运到工人边上
2,工人完成自己负责的部分
而 tomcat 的 Pipeline 实现是这样的:
1,在生产线上的第一个工人拿到生产原料后,二话不说就人给下一个工人,下一个工人模仿第一个工人那样扔给下一个工人,直到最后一个工人,而最后一个工人被安排为上面提过的 StandardValve,他要完成的工作居然是把生产资料运给自己包含的 container 的 Pipeline 上去。
2,四个 container 就相当于有四个生产线(Pipeline),四个 Pipeline 都这么干,直到最后的 StandardWrapperValve 拿到资源开始调用 servlet。完成后返回来,一步一步的 valve 按照刚才丢生产原料是的顺序的倒序一次执行。如此才完成了 tomcat 的 Pipeline 的机制。
2. 手写管道模式实现
在管道中连接一个或者多个阀门,每一个阀门负责一部分逻辑处理,数据按照规定的顺序往下流。此种模式分解了逻辑处理任务,可方便对某个任务单元进行安装、拆卸,提高流程的可拓展性,可重用性,机动性,灵活性。
管道接口
public interface Pipeline {
//获取第一个阀门
Valve getFirst();
Valve getBasic();
//设置阀门
void setBasic(Valve valve);
//添加阀门
void addVave(Valve valve);
}
阀门接口
public interface Valve {
Valve getNext();
void setNext(Valve valve);
void invoke(String handing);
}
阀门实现类
public class FirstValve implements Valve {
protected Valve next = null;
@Override
public Valve getNext() {
return next;
}
@Override
public void setNext(Valve valve) {
this.next = valve;
}
@Override
public void invoke(String request) {
System.out.println("定制阀门1处理");
getNext().invoke(request);
}
}
public class StandardValve implements Valve {
protected Valve next =null; // 每一个都保存next
@Override
public Valve getNext() {
return next;
}
@Override
public void setNext(Valve valve) {
this.next =valve;
}
@Override
public void invoke(String request) {
request = request+"xxoo,";
System.out.println("基础阀门处理");
}
}
管道实现类
public class StandardPipeline implements Pipeline {
//阀门(非基础,定义一个first)
protected Valve first = null;
//基础阀门
protected Valve basic = null;
@Override
public Valve getBasic() {
return basic;
}
@Override
public void setBasic(Valve valve) {
this.basic = valve;
}
@Override
public Valve getFirst() {
return first;
}
//添加阀门,链式构建阀门的执行顺序(先定制、最后基础阀门)
@Override
public void addVave(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);
}
current = current.getNext();
}
}
}
}
测试类
public static void main(String[] args) {
String request = "这个是一个Servlet请求";
//new出一个管道
StandardPipeline pipeline = new StandardPipeline();
//三个阀门(一个基础、2个定制)
StandardValve standardValve = new StandardValve();
FirstValve firstValve = new FirstValve();
SecondValve secondValve = new SecondValve();
//设置基础阀门(定制阀门)
pipeline.setBasic(standardValve);
//设置非基础阀门
pipeline.addVave(firstValve);
pipeline.addVave(secondValve);
//调用对象管道中的第一个阀门
pipeline.getFirst().invoke(request);
}
3. 源码分析
在 CoyoteAdapter 的 service 方法里,由下面这一句就进入 Container 的。
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
来自 org.apache.catalina.core.StandardEngineValve
的 invoke 方法:
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// Select the Host to be used for this Request
Host host = request.getHost(); // 拿到自己的host
if (host == null) {
response.sendError
(HttpServletResponse.SC_BAD_REQUEST,
sm.getString("standardEngine.noHost",
request.getServerName()));
return;
}
if (request.isAsyncSupported()) { // 设置是不支持标记
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
}
// Ask this Host to process this request
host.getPipeline().getFirst().invoke(request, response);
// 感觉StandardEngineValve无法扩展了?, 其实是理解错了, 代码执行到这个invoke时候,说明一定是basic, 因为这个被定义为basic阀门, 我们自定义的只能是非基础阀门, 不然整个管道与管道之间不能保证连通没有问题, basic阀门可以理解成管道之间的接口
}
// 注意, 这里
// org.apache.catalina.core.StandardEngine#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;
}
其他的类似 StandardHostValve、StandardContextValve、StandardWrapperValve
basic阀门可能理解成管道之间或者是任何管道对接的接口, tomcat也只提供了默认的basic阀门, 其他定制需要我们自己添加非基础的阀门
4. Tomcat 中定制阀门
管道机制给我们带来了更好的拓展性,例如,你要添加一个额外的逻辑处理阀门是很容易的。
- 自定义个阀门 PrintIPValve,只要继承 ValveBase 并重写 invoke 方法即可。注意在 invoke 方法中一定要执行调用下一个阀门的操作,否则会出现异常。
public class PrintIPValve extends ValveBase{
@Override public void invoke(Request request, Response response) throws IOException, ServletException {
System.out.println("------自定义阀门 PrintIPValve:"+request.getRemoteAddr());
getNext().invoke(request,response);
}
}
-
配置Tomcat的核心配置文件server.xml,这里把阀门配置到Engine容器下,作用范围就是整个引擎,也可以根据作用范围配置在Host或者是Context 下
<Valve className="org.apache.catalina.valves.PrintIPValve" />
-
源码中是直接可以有效果,但是如果是运行版本,则可以将这个类导出成一个 Jar 包放入 Tomcat/lib 目录下,也可以直接将.class 文件打包进 catalina.jar 包中。
5. Tomcat 中提供常用的阀门
AccessLogValve,请求访问日志阀门,通过此阀门可以记录所有客户端的访问日志,包括远程主机 IP,远程主机名,请求方法,请求协议,会话 ID, 请求时间,处理时长,数据包大小等。它提供任意参数化的配置,可以通过任意组合来定制访问日志的格式。
JDBCAccessLogValve,同样是记录访问日志的阀门,但是它有助于将访问日志通过 JDBC 持久化到数据库中。
ErrorReportValve,这是一个将错误以 HTML 格式输出的阀门
PersistentValve,这是对每一个请求的会话实现持久化的阀门RemoteAddrValve,访问控制阀门。可以通过配置决定哪些 IP 可以访问 WEB 应用
RemoteHostValve,访问控制阀门,通过配置觉得哪些主机名可以访问 WEB 应用
RemoteIpValve,针对代理或者负载均衡处理的一个阀门,一般经过代理或者负载均衡转发的请求都将自己的 IP 添加到请求头”X-Forwarded-For”中,此时,通过阀门可以获取访问者真实的 IP。
SemaphoreValve,这个是一个控制容器并发访问的阀门,可以作用在不同容器上