5.1 Valve 与 Filter:管道上的两道闸 本节摘要:请求在容器里要过两道外观相似的闸——Valve 是容器层阀门,Filter 是应用层过滤器。本节从一次重复日志的故障反例切入,用一张位置对比图讲清两者在挂载层、作用域、配置位置、类可见性上的差异,给出执行顺序的完整链路,并实现一个最小的自定义阀门与一个对等过滤器对照。学完你能正确回答"这个横切逻辑该写成 Valve 还是 Filter"。 请求已过四层指路(第 4 章),在抵达业务代码之前还有两道闸要过。这两道闸长得像、干的事也像,位置却差着一层——分不清它们,日志重复、权限失效这类怪象就无从下手。 从一次重复的日志说起 某团队发现每个请求的访问日志被记了两次,起初以为是日志框架配置问题,查了一小时无果。
本节摘要:请求在容器里要过两道外观相似的闸——Valve 是容器层阀门,Filter 是应用层过滤器。本节从一次重复日志的故障反例切入,用一张位置对比图讲清两者在挂载层、作用域、配置位置、类可见性上的差异,给出执行顺序的完整链路,并实现一个最小的自定义阀门与一个对等过滤器对照。学完你能正确回答"这个横切逻辑该写成 Valve 还是 Filter"。
请求已过四层指路(第 4 章),在抵达业务代码之前还有两道闸要过。这两道闸长得像、干的事也像,位置却差着一层——分不清它们,日志重复、权限失效这类怪象就无从下手。
某团队发现每个请求的访问日志被记了两次,起初以为是日志框架配置问题,查了一小时无果。真正的答案:运维在 Host 层配了一个 AccessLogValve,而应用自己又带了一个做访问统计的 Filter,两者都在响应路径上各自记了一笔。修法不难——二选一,但要判断"该删哪个",就得知道两道闸各自站在哪、看见什么。

四条差异一次记全。挂载层:Valve 挂在 Engine、Host、Context 任一层,Filter 只存在于应用内。作用域:Valve 按挂载点决定管多大范围(Host 级阀门对该主机所有应用生效),Filter 天然只管自己。配置位置:Valve 在 server.xml 或 context 片段,Filter 在 web.xml 或注解。类可见性:Valve 的实现类对容器负责、加载自容器侧,看不见应用的业务类;Filter 是应用的一部分,能使用应用的会话、参数与依赖。回到开头那个故障:访问统计要区分登录用户就该是 Filter,纯流量日志该是 Valve——当时两处都写了"日志",职责重叠才是病根。
两道闸不是并列而是串联,完整顺序是:Engine 阀门链、Host 阀门链、Context 阀门链、过滤器链、Servlet。每一层内部又有自己的次序——Valve 按配置先后排成链,Filter 按 web.xml 里 filter-mapping 的出现顺序排成链。调试时在两道闸各打一行日志,顺序一目了然。
配置先看 Valve 侧。内置阀门好几个,两个最常用:AccessLogValve 写访问日志(2.2 已见过挂点),RemoteIpValve 还原前置代理传来的真实 IP(1.2 用过)。再来一个更贴近运维的——用内置阀门按网段拒绝访问。
<!-- server.xml:Host 层挂阀门 拒绝内网某段对管理类路径的访问 --> <Valve className="org.apache.catalina.valves.RemoteAddrValve" deny="10\.8\.9\..*"/>
Filter 侧用等价的访问控制写一遍,对照感立刻出来。
// 应用内的过滤器:只放行登录用户 其余重定向到登录页 import jakarta.servlet.*; import jakarta.servlet.http.*; import java.io.IOException; public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; Object user = request.getSession().getAttribute("loginUser"); if (user == null && !request.getRequestURI().contains("/login")) { response.sendRedirect("/login"); return; // 链在此中断 请求到不了 Servlet } chain.doFilter(req, resp); // 放行 交给链中下一个 } }
注意最后一行的两个分支:不放行就直接 return,请求到此为止;放行则调用链的 doFilter,控制权流向下一环。能否主动中断请求是两道闸的共同能力,但 Valve 里断掉时应用毫无感知,Filter 里断掉时可以给用户一个友好页面——体验差异也是选型依据。
内置阀门不够用时,自定义一个并不难。需求:把每个请求的处理耗时打进独立日志。实现类继承基类,覆写 invoke 方法,手动调用下一环。
// 放进容器的 lib 目录 供 Valve 使用 import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; import org.apache.catalina.valves.ValveBase; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import jakarta.servlet.ServletException; import java.io.IOException; public class TimingValve extends ValveBase { private static final Log log = LogFactory.getLog(TimingValve.class); @Override public void invoke(Request request, Response response) throws IOException, ServletException { long start = System.currentTimeMillis(); try { getNext().invoke(request, response); // 先放行 让后面各站处理 } finally { long cost = System.currentTimeMillis() - start; log.info(request.getRequestURI() + " 耗时 " + cost + " 毫秒"); } } }
<!-- server.xml:挂到 Host 层 全主机生效 --> <Valve className="demo.TimingValve"/>
finally 块保证异常路径也记时长——这类"城墙级监控"正该由 Valve 承担:它不依赖任何应用,换应用、下应用都不影响计时。同理,网关级的限流、熔断埋点也适合放这一层。
⚠️ 常见坑:自定义 Valve 写好后启动报类找不到——因为它必须对容器的公共类加载器可见,放进应用的 WEB-INF 是无效的,正确位置是容器的 lib 目录。Filter 恰好相反。放错位置的报错信息往往指向别处,记住"谁的类放谁家"。
闸门认全了,下一节专讲其中裁决最重的一道——身份认证。Realm 这个档案库如何被 tomcat-users 与 web.xml 一唱一和地点名。