Pipeline和Valve
Tomcat 中的 Pipeline 与 Valve:请求处理的责任链模式
Tomcat 的 Pipeline(管道)和 Valve(阀门)是其核心组件,用于处理请求和响应的流程控制。它们基于责任链模式设计,将请求处理的不同任务拆解为独立的阀门,通过管道串联执行,实现了请求处理逻辑的解耦与灵活扩展。本文将深入解析 Pipeline 与 Valve 的工作原理、结构及配置方式。
Pipeline 与 Valve 的核心概念
基本定义
- Pipeline(管道):
每个 Tomcat 容器(Engine、Host、Context、Wrapper)都包含一个 Pipeline,它是一个任务执行链,负责按顺序调用一系列 Valve 处理请求。 - Valve(阀门):
每个 Valve 代表一个具体的处理任务(如日志记录、权限校验、请求转发等),类似于过滤器(Filter),但作用于 Tomcat 容器级别而非 Web 应用级别。 - Basic Valve(基础阀门):
每个 Pipeline 必须包含一个 Basic Valve,作为管道的最终处理器(责任链的终点),负责执行容器的核心逻辑(如Wrapper的 Servlet 调用、Host的虚拟主机路由)。
设计思想:责任链模式
Pipeline 与 Valve 的设计借鉴了责任链模式,其核心特点是:
- 每个 Valve 只处理自己负责的任务,处理完成后将请求传递给下一个 Valve。
- 可通过添加 / 移除 Valve 灵活扩展请求处理逻辑,无需修改原有代码。
- 不同容器的 Pipeline 独立工作,形成层级化的处理流程(如 Engine → Host → Context → Wrapper)。
Pipeline 与 Valve 的结构与实现
容器与 Pipeline 的对应关系
Tomcat 的四大容器(从顶层到应用层)均内置 Pipeline:
| 容器类型 | 对应的 Pipeline | Basic Valve 作用 |
|---|---|---|
Engine |
StandardPipeline |
负责将请求路由到对应的 Host 容器。 |
Host |
StandardPipeline |
负责将请求路由到对应的 Context 容器。 |
Context |
StandardPipeline |
负责将请求路由到对应的 Wrapper 容器。 |
Wrapper |
StandardPipeline |
负责加载并调用 Servlet 处理请求。 |
示例:StandardEngine 中 Pipeline 的初始化代码:
public class StandardEngine extends ContainerBase implements Engine {
// 每个容器初始化时创建对应的 Pipeline
protected final Pipeline pipeline = new StandardPipeline(this);
// 设置 Engine 的 Basic Valve
public StandardEngine() {
pipeline.setBasic(new StandardEngineValve());
}
}
Valve 的链式存储与调用
Valve 在 Pipeline 中以单向链表的形式存储,通过 getNext() 和 setNext() 维护顺序:
public class StandardPipeline implements Pipeline {
private Valve first = null; // 第一个 Valve
private Valve basic = null; // Basic Valve(最后一个)
// 添加 Valve 到管道(插入到 first 与 basic 之间)
public void addValve(Valve valve) {
if (first == null) {
first = valve;
valve.setNext(basic); // 新 Valve 的下一个是 basic
} else {
Valve current = first;
// 遍历到最后一个非 basic 的 Valve
while (current.getNext() != basic) {
current = current.getNext();
}
current.setNext(valve); // 当前 Valve 的下一个指向新 Valve
valve.setNext(basic); // 新 Valve 的下一个指向 basic
}
}
}
调用流程:
当请求进入容器时,Pipeline 从第一个 Valve 开始调用,依次执行每个 Valve 的 invoke() 方法,最终到达 Basic Valve:
// Valve 接口的核心方法
public interface Valve {
void invoke(Request request, Response response) throws IOException, ServletException;
Valve getNext();
void setNext(Valve valve);
}
// 示例 Valve 的实现(简化版)
public class MyValve implements Valve {
private Valve next;
public void invoke(Request request, Response response) throws IOException, ServletException {
// 1. 执行当前 Valve 的任务(如日志记录)
System.out.println("处理请求:" + request.getRequestURI());
// 2. 传递给下一个 Valve
if (next != null) {
next.invoke(request, response);
}
}
// getNext() 和 setNext() 实现略
}
请求处理的完整流程(多层 Pipeline 协作)
一个 HTTP 请求在 Tomcat 中的处理需要经过多层容器的 Pipeline,流程如下:
- 请求进入 Engine 容器:
Engine的 Pipeline 执行一系列 Valve(如AccessLogValve记录访问日志)。- 最终由
StandardEngineValve(Basic Valve)根据域名将请求路由到对应的Host。
- 请求进入 Host 容器:
Host的 Pipeline 执行 Valve(如SingleSignOn处理单点登录)。- 由
StandardHostValve(Basic Valve)根据请求路径路由到对应的Context。
- 请求进入 Context 容器:
Context的 Pipeline 执行 Valve(如RemoteAddrValve限制 IP 访问)。- 由
StandardContextValve(Basic Valve)根据 Servlet 映射路由到对应的Wrapper。
- 请求进入 Wrapper 容器:
Wrapper的 Pipeline 执行 Valve(如RequestDumperValve打印请求详情)。- 由
StandardWrapperValve(Basic Valve)加载并调用目标 Servlet 的service()方法。
示意图:
请求 → Engine Pipeline [Valve1 → Valve2 → ... → StandardEngineValve]
→ Host Pipeline [ValveA → ValveB → ... → StandardHostValve]
→ Context Pipeline [ValveX → ValveY → ... → StandardContextValve]
→ Wrapper Pipeline [ValveM → ValveN → ... → StandardWrapperValve]
→ 调用 Servlet
Valve 的配置与常用实现
配置 Valve(以 Host 为例)
Valve 可在 server.xml 中对应容器下配置,例如为 Host 添加访问日志 Valve:
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
<!-- 访问日志 Valve:记录请求信息到日志文件 -->
<Valve
className="org.apache.catalina.valves.AccessLogValve"
directory="logs"
prefix="localhost_access_log"
suffix=".txt"
pattern="%h %l %u %t "%r" %s %b"
/>
<!-- 远程地址限制 Valve:仅允许 192.168.x.x 网段访问 -->
<Valve
className="org.apache.catalina.valves.RemoteAddrValve"
allow="192\.168\.\d+\.\d+"
/>
</Host>
className:Valve 的实现类全路径。- 其他属性:Valve 特有的配置(如日志路径、允许的 IP 规则)。
常用 Valve 实现
Tomcat 内置了多种实用 Valve,覆盖日志、安全、性能等场景:
| Valve 类名 | 作用说明 |
|---|---|
AccessLogValve |
记录访问日志(IP、请求路径、响应码等),支持自定义格式。 |
RemoteAddrValve |
根据客户端 IP 地址允许 / 拒绝访问(如限制内网访问)。 |
RemoteHostValve |
根据客户端主机名允许 / 拒绝访问。 |
SingleSignOn |
实现多 Web 应用间的单点登录(共享认证信息)。 |
RequestDumperValve |
打印请求 / 响应的详细信息(用于调试)。 |
ErrorReportValve |
生成友好的错误页面(如 404、500 错误)。 |
SlowAccessValve |
记录慢请求(超过指定时间的请求),用于性能排查。 |
自定义 Valve 开发
若内置 Valve 无法满足需求,可自定义 Valve 扩展功能,步骤如下:
实现 Valve 接口:
继承ValveBase(简化实现),重写invoke()方法:import org.apache.catalina.Valve; import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; import org.apache.catalina.valves.ValveBase; import javax.servlet.ServletException; import java.io.IOException; public class CustomValve extends ValveBase { public void invoke(Request request, Response response) throws IOException, ServletException { // 1. 处理请求前的逻辑(如计时开始) long start = System.currentTimeMillis(); // 2. 传递给下一个 Valve if (getNext() != null) { getNext().invoke(request, response); } // 3. 处理响应后的逻辑(如计算耗时) long end = System.currentTimeMillis(); System.out.println("请求 " + request.getRequestURI() + " 耗时:" + (end - start) + "ms"); } }编译与部署:
将类打包为 JAR,放入$CATALINA_BASE/lib目录。配置使用:
在server.xml中对应容器添加自定义 Valve:<Context path="/myapp" docBase="myapp"> <Valve className="com.example.CustomValve" /> </Context>
Pipeline 与 Filter 的区别
尽管 Valve 和 Filter 都用于请求处理,但二者存在本质区别:
| 对比维度 | Valve | Filter |
|---|---|---|
| 作用范围 | Tomcat 容器级别(Engine/Host 等) | Web 应用级别(仅当前应用) |
| 配置位置 | server.xml 或 context.xml |
应用的 web.xml 或注解 |
| 调用时机 | 在容器处理的早期阶段(路由前) | 在 Servlet 调用前(路由后) |
| 依赖环境 | 依赖 Tomcat API(Request 等) |
依赖 Servlet 规范 API |
| 扩展性 | 可影响所有应用(如 Engine 级别) | 仅影响当前应用 |