在 Tomcat 对 NIO 和 HTTP 协议的实现 一文中描述了对 TCP 连接和解析 HTTP 请求的处理,接下来会进一步封装请求交给 Catalina 容器处理以生成响应。在此之前首先了解一下 web 应用程序部署后在容器里的体现。
容器内部的 web 应用程序
一个 web 应用由 servlet、html页面、类和其他资源组成,web程序的根目录是一个特定的路径,如 /examples,以这个前缀开始的请求都被路由到 examples 应用的 ServletContext 环境中。
默认情况下,容器启动时会部署 webapps 文件夹下的 war 文件或者子文件夹,运行时 Engine 有一个后台线程也会定时扫描部署,部署的本质就是解析 web.xml,创建 Context、Wrapper 等对象并组装的过程,部署完成后的内部简单结构:
上图类似于树结构,根据请求 URI,自顶向下总能找到一个 Servlet 来处理,每个应用程序都有一个默认的 DefaultServlet 来处理静态资源或者是没有匹配到 Servlet 的请求。
容器内请求的处理
容器主要通过 Pipeline 以流水线外加 valve 阀门的机制联合各组件处理请求,具体结构如下:
默认情况下,容器处理过程为:
- Connector 接收并解析 HTTP 请求,封装成 Request 对象,交给 Adapter,然后 Adapter 通过 Mapper 对象根据请求 host 头域和请求 URL 找到匹配的虚拟主机、web应用程序(ServletContext)以及对应的 Wrapper 对象,最后 调用 Engine 的 Pipeline 中 valves 的 invoke 方法,请求转入容器。
- Engine 默认只有 StandardEngineValve,它只是简单将请求转入到其匹配的 Host 子容器的 Pipeline 中。
- Host 默认有两个 Valve,处理过程为:
- 首先,由 ErrorReportValve 处理,它主要调用下一个 Valve,处理有错误的响应;
- 然后,由 StandardHostValve 处理,它获取请求匹配的应用程序 Context,将当前线程的 ClassLoader 绑定为 Context关联的 WebappClassLoader;
- 最后,将请求交给 Context 的 Pipeline 处理,当 Context 处理完毕返回时,此 Valve 会还原当前线程的 ClassLoader。
- Context 默认只有 StandardContextValve,处理过程为:
- 首先,它判断请求路径是否包含 META-INF或者 WEB-INF,是的话直接返回404错误;
- 然后,获取 web.xml 中配置的 ServletRequestListener,触发其初始化的方法;
- 最后,拿到封装 Servlet 的 Wrapper 对象,将请求转入它的 Pipeline 处理,处理完毕销毁 RequestListener。
- Wrapper 默认的阀门是 StandardWrapperValve,首先,它检查应用程序和 Wrapper是否可用,如果可用,使用 Wrapper 的 allocate() 方法申请 Servlet 实例,有两种情况:
- 如果 Servlet 已经加载初始化则直接返回,没有则使用Context关联的 WebappClassLoader 加载并初始化,使用的都是同一个 Servlet 对象;
- 如果 Servlet 实现了 SingleThreadedModel 接口,那么对于每个请求线程返回不同的 Servlet 实例,默认Servlet对象池大小为20,也就是说相同请求并发超过20,剩余的将等待可用的Servlet;
接下来,创建配置的 FilterChain,依次调用过滤器链中的每个 Filter 的 doFilter 方法,最后一个 Filter 处理完毕后,调用 Servlet.service() 方法,根据请求方法调用 doGet 或者 doPost;最后,当 service 方法返回,过滤器链依次返回并按需处理响应对象。
由上可以看出,核心就在于 Pipeline 和 FilterChain,上图 Servlet 处理完毕后的返回箭头,表示这些 Vavle和Filter 它们既能处理请求,也能处理响应,这是借助了线程调用栈实现的,也是典型的责任链模式的实现,此模式的应用相当广泛。
小结
在分析源码的过程中发现,直接加断点跟请求不难,难的在于一开始在脑海里无法理清这么多对象的关联关系,甚至有时候明知道这个类会处理,却不知调用点在哪。理清这些的关键在于理解 Tomcat 对配置文件的解析,触发 Lifecycle 生命周期事件的时机以及合适的断点位置。
Tomcat 源码还是挺多的,过于深入细节反而得不偿失,也欢迎提一些问题点,以此为切入点来讨论和分析源码。接下来,会对 Tomcat 高级特性进行分析,比如集群的实现原理、Session的处理、连接池等等,因为我使用的是 Tomcat6 版本的源码,后续也会分析 Tomcat7 做出的优化和重构。
其他关联文章:
Tomcat 架构概述
Tomcat 启动初始化和停止
Tomcat 对 NIO 和 HTTP 协议的实现
Tomcat 源码注释