5.1 Tomcat Valve (阀门) 5.1 Tomcat Valve (阀门) 5.1.1 Valve 的概念与作用 在Tomcat的请求处理流程中,当一个HTTP请求到达Tomcat服务器后,它会经过一系列组件的处理才能最终到达Servlet容器,并被相应的Servlet处理。Valve就位于这个处理管道之中,它像一个“阀门”一样,拦截并处理流经它的请求和响应。 核心概念: 请求处理管道: Tomcat使用管道-阀门(Pipeline-Valve)设计模式来处理HTTP请求。Pipeline代表请求处理管道,而Valve则是管道中的一个个处理环节。 拦截器: Valve本质上是一种拦截器,它可以拦截到达容器的请求,并在请求被Servlet处理之前或之后执行特定的逻辑。
在Tomcat的请求处理流程中,当一个HTTP请求到达Tomcat服务器后,它会经过一系列组件的处理才能最终到达Servlet容器,并被相应的Servlet处理。Valve就位于这个处理管道之中,它像一个“阀门”一样,拦截并处理流经它的请求和响应。
核心概念:
请求处理管道: Tomcat使用管道-阀门(Pipeline-Valve)设计模式来处理HTTP请求。Pipeline代表请求处理管道,而Valve则是管道中的一个个处理环节。
拦截器: Valve本质上是一种拦截器,它可以拦截到达容器的请求,并在请求被Servlet处理之前或之后执行特定的逻辑。
可插拔: Valve是可插拔的组件,可以根据需求灵活地添加到Tomcat的各个容器(Engine, Host, Context, Wrapper)中,实现定制化的请求处理流程。
职责链模式: Pipeline实际上是职责链模式的一种实现,请求会依次经过Pipeline中配置的Valve,每个Valve都有机会处理请求,并将请求传递给下一个Valve。
Valve 的主要作用:
请求预处理: 在请求到达Servlet之前,Valve可以进行请求的预处理,例如字符编码转换、请求参数校验、身份认证、访问控制等。
响应后处理: 在Servlet处理完请求并生成响应后,Valve可以进行响应的后处理,例如添加响应头、压缩响应内容、记录访问日志等。
请求转发/重定向: Valve可以根据特定条件将请求转发到其他的Servlet或资源,或者进行重定向。
错误处理: Valve可以捕获请求处理过程中发生的异常,并进行统一的错误处理,例如跳转到错误页面、记录错误日志等。
性能监控: Valve可以记录请求处理的耗时、资源使用情况等信息,用于性能监控和分析。
安全增强: Valve可以实现诸如IP地址过滤、请求频率限制、安全审计等安全增强功能。
Tomcat的Valve架构基于 org.apache.catalina.Valve 接口和 org.apache.catalina.Pipeline 接口。
Valve 接口:
org.apache.catalina.Valve 接口是所有Valve组件必须实现的接口。它定义了Valve的基本行为,最核心的方法是 invoke(Request request, Response response)。
public interface Valve { // ... 其他方法 ... /** * Execute a periodic operation, such as scanning for invalid sessions, * expiring timers, etc. */ public void backgroundProcess(); /** * Return descriptive information about this Valve implementation. */ public String getInfo(); /** * Return the next Valve in the pipeline associated with this Valve, * if any. */ public Valve getNext(); /** * Set the next Valve in the pipeline associated with this Valve. * * @param valve The new next valve */ public void setNext(Valve valve); /** * Execute a periodic operation, such as scanning for invalid sessions, * expiring timers, etc. * <p> * <b>IMPLEMENTATION NOTE</b>: It is strongly recommended that Valve * implementations write log messages using the facilities of their * associated Manager or Container, rather than to System.out or * System.err. This allows a Catalina container to redirect log output * appropriately. * * @param request The servlet request being processed * @param response The servlet response being created * * @exception IOException if an input/output error occurs * @exception ServletException if a servlet error occurs */ public void invoke(Request request, Response response) throws IOException, ServletException; }
invoke(Request request, Response response): 这是Valve的核心方法。当请求到达Valve时,Tomcat容器会调用这个方法。Valve的实现需要在该方法中完成自身的处理逻辑。
请求和响应对象: 方法参数 request 和 response 分别是Tomcat封装的请求和响应对象,提供了访问HTTP请求和响应的各种信息和操作的方法。
调用链: 在 invoke() 方法中,Valve可以选择是否将请求传递给Pipeline中的下一个Valve。通常,Valve会调用 getNext().invoke(request, response) 将请求传递给下一个Valve,形成Valve的调用链。如果不调用 getNext().invoke(),则请求处理流程将被Valve中断。
Pipeline 接口:
org.apache.catalina.Pipeline 接口定义了Valve的管道,负责管理和执行Valve链。每个Tomcat容器(Engine, Host, Context, Wrapper)都关联一个Pipeline。
public interface Pipeline { // ... 其他方法 ... /** * Add a Valve to the end of the pipeline. * * @param valve Valve to be added * * @exception IllegalArgumentException if this Valve is not of a * type that is allowed to be added to this Pipeline. * @exception IllegalArgumentException if the specified Valve is null */ public void addValve(Valve valve) throws IllegalArgumentException; /** * Get the Valves associated with this Pipeline. * * @return the valves associated with this Pipeline */ public Valve[] getValves(); /** * Return the basic Valve (if any) associated with this Pipeline. */ public Valve getBasic(); /** * Set the basic Valve (usually the last one in the pipeline). * * @param valve The new basic valve */ public void setBasic(Valve valve); /** * Return the Valve instance that has been distinguished as the basic Valve * (if any). * * @return the basic Valve */ public Valve getFirst(); /** * Remove a Valve from the pipeline. * * @param valve Valve to be removed * * @exception IllegalArgumentException if this Valve is not of a * type that is allowed to be removed from this Pipeline. * @exception IllegalArgumentException if the specified Valve is null */ public void removeValve(Valve valve) throws IllegalArgumentException; /** * Invoke the first Valve in this pipeline's processing chain, passing * the specified request and response. * * @param request the request to be processed * @param response the response to be created * * @exception IOException if an input/output error occurs * @exception ServletException if a servlet error occurs * @exception Throwable if a processing error occurs */ public void invoke(Request request, Response response) throws IOException, ServletException, Throwable; }
addValve(Valve valve): 向Pipeline中添加Valve。Valve会按照添加的顺序形成调用链。
getValves(): 获取Pipeline中所有的Valve。
invoke(Request request, Response response): 启动Pipeline的请求处理流程。当Tomcat容器接收到请求后,会调用Pipeline的 invoke() 方法,该方法会依次调用Pipeline中的每个Valve的 invoke() 方法。
Valve 工作流程:
请求接收: Tomcat容器(Engine, Host, Context, Wrapper)接收到HTTP请求。
Pipeline 启动: 容器调用与其关联的Pipeline的 invoke() 方法,启动请求处理流程。
Valve 链式调用: Pipeline的 invoke() 方法会依次调用Pipeline中配置的每个Valve的 invoke() 方法。
Valve 处理: 每个Valve的 invoke() 方法执行自身的处理逻辑,例如日志记录、权限校验等。
传递到下一个 Valve: 大部分Valve在完成自身处理后,会调用 getNext().invoke(request, response) 将请求传递给Pipeline中的下一个Valve。
Basic Valve (Servlet 容器): Pipeline的最后一个Valve通常是Basic Valve,它负责将请求传递给Servlet容器进行Servlet处理。
Servlet 处理: 请求到达Servlet容器,被相应的Servlet处理,生成响应。
响应返回 Valve 链: 响应生成后,会沿着Valve链逆向返回,每个Valve的 invoke() 方法可以进行响应的后处理。
响应发送: 最终,响应被发送回客户端。
Valve 的分类:
Tomcat Valve 可以根据不同的维度进行分类:
按功能分类:
日志 Valve: AccessLogValve, RequestDumperValve 等,用于记录请求访问日志、调试信息。
安全 Valve: RemoteAddrValve, RemoteHostValve, JSecurityValve 等,用于实现IP地址过滤、主机名过滤、安全认证等。
会话管理 Valve: PersistentValve, StandardSessionValve 等,用于会话持久化、会话管理。
错误处理 Valve: ErrorReportValve,用于处理请求处理过程中发生的错误。
性能监控 Valve: 自定义 Valve 可以实现性能监控功能。
按实现方式分类:
标准 Valve (Standard Valve): Tomcat内置的 Valve,例如 AccessLogValve, RemoteAddrValve 等。
自定义 Valve (Custom Valve): 开发者根据自身需求实现的 Valve。
按容器级别分类:
Engine Valve: 配置在 <Engine> 容器下的 Valve,作用于整个Tomcat实例的所有Host和Context。
Host Valve: 配置在 <Host> 容器下的 Valve,作用于该Host下的所有Context。
Context Valve: 配置在 <Context> 容器下的 Valve,作用于该Context下的所有Servlet。
Wrapper Valve: 配置在 <Wrapper> 容器下的 Valve,作用于单个Servlet。Wrapper Valve 相对较少使用,通常在Context或更高级别配置 Valve 即可满足需求。
Tomcat Valve 的配置通常在 server.xml 文件中进行。可以根据 Valve 的作用范围,将其配置在不同的容器标签下:<Engine>, <Host>, <Context>.
配置语法:
<Engine ...> <Valve className="your.custom.ValveClassName" attribute1="value1" attribute2="value2" ... /> ... </Engine> <Host ...> <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log." suffix=".txt" pattern="%h %l %u %t "%r" %s %b" resolveHosts="false"/> ... </Host> <Context path="/your-app" ...> <Valve className="org.apache.catalina.valves.RemoteAddrValve" allow="127\.0\.0\.1|192\.168\.\d+\.\d+"/> ... </Context>
<Valve> 标签: 用于声明一个 Valve。
className 属性: 指定 Valve 的完整类名,必须是实现了 org.apache.catalina.Valve 接口的类。
其他属性: Valve 可以根据自身的需求定义其他的配置属性,在 <Valve> 标签中使用属性名-值对进行配置。这些属性的值会在 Valve 初始化时被设置到 Valve 实例中。
配置位置:
Engine Valve: 配置在 <Engine> 标签内部,对整个Tomcat实例生效。通常用于全局性的功能,例如全局访问日志、全局安全策略等。
Host Valve: 配置在 <Host> 标签内部,对该Host下的所有Web应用生效。通常用于Host级别的功能,例如Host级别的访问日志、Host级别的安全策略等。
Context Valve: 配置在 <Context> 标签内部,只对该Web应用生效。通常用于Web应用级别的功能,例如应用级别的访问日志、应用级别的安全策略、自定义的请求处理逻辑等。
标准 Valve 示例 - AccessLogValve:
AccessLogValve 是 Tomcat 内置的用于记录请求访问日志的 Valve。以下是一个配置 AccessLogValve 的示例:
<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" resolveHosts="false"/> <Valve className="org.apache.catalina.valves.ErrorReportValve" /> <Context path="/my-app" docBase="my-app" reloadable="true"> </Context> </Host>
className="org.apache.catalina.valves.AccessLogValve": 指定使用 AccessLogValve。
directory="logs": 指定日志文件存放目录为 Tomcat 的 logs 目录。
prefix="localhost_access_log.": 指定日志文件名前缀。
suffix=".txt": 指定日志文件后缀。
pattern="%h %l %u %t "%r" %s %b": 指定日志格式,%h 表示客户端主机名,%l 表示远程登录名,%u 表示远程用户,%t 表示日期和时间,%r 表示请求行,%s 表示状态码,%b 表示响应内容长度。
resolveHosts="false": 是否解析主机名,设置为 false 可以提高性能。
标准 Valve 示例 - RemoteAddrValve:
RemoteAddrValve 用于根据客户端IP地址进行访问控制。以下是一个配置 RemoteAddrValve 的示例,只允许来自 127.0.0.1 和 192.168.x.x 网段的IP地址访问:
<Context path="/my-app" docBase="my-app" reloadable="true"> <Valve className="org.apache.catalina.valves.RemoteAddrValve" allow="127\.0\.0\.1|192\.168\.\d+\.\d+"/> </Context>
className="org.apache.catalina.valves.RemoteAddrValve": 指定使用 RemoteAddrValve。
allow="127\.0\.0\.1|192\.168\.\d+\.\d+": 使用正则表达式指定允许访问的IP地址,| 表示或,\. 需要转义,\d+ 表示一个或多个数字。
为了更好地理解 Valve 的工作原理和应用,我们来创建一个简单的自定义 Valve,实现记录请求处理时间的日志功能。
1. 创建自定义 Valve 类:
创建一个 Java 类 RequestTimeValve,实现 org.apache.catalina.Valve 接口。
package your.custom.valve; import org.apache.catalina.Valve; import org.apache.catalina.ValveContext; import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import javax.servlet.ServletException; import java.io.IOException; public class RequestTimeValve implements Valve { private static final Log log = LogFactory.getLog(RequestTimeValve.class); protected Valve next = null; @Override public Valve getNext() { return next; } @Override public void setNext(Valve valve) { this.next = valve; } @Override public void invoke(Request request, Response response, ValveContext context) throws IOException, ServletException { long startTime = System.currentTimeMillis(); try { // 调用下一个 Valve,继续请求处理流程 context.invokeNext(request, response); } finally { long endTime = System.currentTimeMillis(); long elapsedTime = endTime - startTime; String requestURI = request.getRequestURI(); String remoteAddr = request.getRemoteAddr(); log.info("Request URI: " + requestURI + ", Remote Address: " + remoteAddr + ", Processing Time: " + elapsedTime + "ms"); } } @Override public void backgroundProcess() { // 不需要后台处理,可以留空 } @Override public String getInfo() { return "RequestTimeValve/1.0"; } @Override public boolean isAsyncSupported() { return false; // 简单示例,不支持异步 } }
代码详解:
package your.custom.valve;: 指定 Valve 类的包名。
import ...;: 导入需要的类。
public class RequestTimeValve implements Valve: 定义 RequestTimeValve 类,实现 Valve 接口。
private static final Log log = LogFactory.getLog(RequestTimeValve.class);: 使用 Tomcat 的日志框架 JULI 获取 Logger 实例。
protected Valve next = null;: 保存 Pipeline 中的下一个 Valve。
getNext(), setNext(): 实现 Valve 接口的 getNext() 和 setNext() 方法,用于获取和设置下一个 Valve。
invoke(Request request, Response response, ValveContext context): Valve 的核心方法。
long startTime = System.currentTimeMillis();: 记录请求开始时间。
context.invokeNext(request, response);: 调用 ValveContext.invokeNext() 方法将请求传递给 Pipeline 中的下一个 Valve。注意这里使用的是 ValveContext 而不是 getNext().invoke(),ValveContext 是 Tomcat 内部使用的接口,用于 Valve 调用链的管理。
finally { ... }: 使用 finally 块确保无论请求处理是否发生异常,都会执行后处理逻辑。
long endTime = System.currentTimeMillis();: 记录请求结束时间。
long elapsedTime = endTime - startTime;: 计算请求处理耗时。
String requestURI = request.getRequestURI();, String remoteAddr = request.getRemoteAddr();: 获取请求 URI 和客户端 IP 地址。
log.info(...): 使用 Logger 记录请求处理信息,包括请求 URI、客户端 IP 地址和处理时间。
backgroundProcess(), getInfo(), isAsyncSupported(): 实现 Valve 接口的其他方法,在本示例中,backgroundProcess() 和 isAsyncSupported() 可以留空或返回默认值,getInfo() 返回 Valve 的描述信息。
2. 打包自定义 Valve:
将 RequestTimeValve.java 编译成 .class 文件,并将其打包成 JAR 文件,例如 request-time-valve.jar。
3. 部署自定义 Valve:
将 request-time-valve.jar 文件复制到 Tomcat 的 lib 目录下。
4. 配置 Tomcat 使用自定义 Valve:
修改 server.xml 文件,在需要应用 RequestTimeValve 的容器中添加 <Valve> 配置。例如,在 <Context> 容器中添加:
<Context path="/my-app" docBase="my-app" reloadable="true"> <Valve className="your.custom.valve.RequestTimeValve"/> </Context>
className="your.custom.valve.RequestTimeValve": 指定自定义 Valve 的完整类名。5. 测试自定义 Valve:
启动 Tomcat 服务器,访问 http://localhost:8080/my-app/ (假设 my-app 是你部署的应用),查看 Tomcat 日志文件 (logs 目录下),应该能看到类似以下的日志输出:
... your.custom.valve.RequestTimeValve - Request URI: /my-app/, Remote Address: 127.0.0.1, Processing Time: 123ms
这表明自定义 Valve RequestTimeValve 已经成功部署并开始工作,记录了请求的处理时间。
Valve 的高级应用:
细粒度的访问控制: 可以根据请求的 URI、HTTP 方法、请求头、请求参数等信息,实现更细粒度的访问控制策略。
请求改写与增强: 可以修改请求的 URI、请求头、请求参数等信息,或者向请求中添加额外的属性,实现请求的改写和增强。
响应内容过滤与修改: 可以过滤或修改响应的内容,例如敏感信息脱敏、内容压缩、内容转换等。
自定义错误页面: 可以根据不同的错误类型,返回自定义的错误页面,提升用户体验。
集成第三方服务: 可以在 Valve 中集成第三方服务,例如调用外部 API 进行身份认证、日志收集、监控告警等。
使用 Valve 的注意事项:
性能影响: Valve 会增加请求处理的开销,特别是复杂的 Valve 逻辑可能会显著降低性能。需要仔细评估 Valve 的性能影响,并进行必要的优化。
Valve 的顺序: Pipeline 中 Valve 的顺序非常重要,不同的顺序可能会导致不同的处理结果。需要仔细考虑 Valve 的执行顺序,确保 Valve 链的正确性。
异常处理: 在 Valve 的 invoke() 方法中,需要妥善处理可能发生的异常,避免异常导致请求处理流程中断或Tomcat服务器崩溃。可以使用 try-catch 块捕获异常,并进行日志记录或错误处理。
线程安全: Valve 实例是单例的,在多线程环境下会被并发访问。需要确保 Valve 的实现是线程安全的,避免出现线程安全问题。
避免过度使用: Valve 虽然强大,但不宜过度使用。过多的 Valve 会增加请求处理的复杂性和开销,降低性能。应该根据实际需求,合理选择和配置 Valve。
Tomcat Valve 是一个强大的组件,它允许开发者在 Tomcat 的请求处理管道中插入自定义的处理逻辑,实现各种高级特性。通过深入理解 Valve 的概念、架构、工作原理和配置使用方法,开发者可以灵活地扩展和定制 Tomcat 的功能,满足各种应用场景的需求。