bigywater.tech
Java · 372

Servlet Filter 链:请求如何经过多层处理

过滤器看起来就是 doFilter 前后各写点代码,但顺序、短路、响应是否已提交、派发类型与异步都有细节。这篇用真实的容器把每一个都跑出来。

声明顺序
过滤器链按声明顺序执行,精确路径的过滤器若后声明则在通配过滤器之后
500 vs 200
chain.doFilter 之后 setStatus(500):小响应生效,20 万字符的大响应已提交所以仍是 200 且 X-Late 头丢失
0 ms vs 361 ms
异步请求里过滤器的「后」执行时的经过时间与客户端实际等待时间

链的顺序与匹配

实测 · 本机执行输出
  [order]
  启动时:
        A(/*).init()
        B(/app/*).init()
        C(精确 /app/x).init()
        D(/other).init()
        E(*.txt).init()
  GET /app/x:200 “hello from hello” Content-Length=16
    事件:
        A(/*) 前(REQUEST /app/x)
        B(/app/*) 前(REQUEST /app/x)
        C(精确 /app/x) 前(REQUEST /app/x)
        servlet hello 处理 /app/x 的 REQUEST
        C(精确 /app/x) 后
        B(/app/*) 后
        A(/*) 后
  GET /app/y:200 “hello from hello” Content-Length=16
    事件:
        A(/*) 前(REQUEST /app/y)
        B(/app/*) 前(REQUEST /app/y)
        servlet hello 处理 /app/y 的 REQUEST
        B(/app/*) 后
        A(/*) 后
  GET /other:200 “hello from other” Content-Length=16
    事件:
        A(/*) 前(REQUEST /other)
        D(/other) 前(REQUEST /other)
        servlet other 处理 /other 的 REQUEST
        D(/other) 后
        A(/*) 后
  GET /nothing.txt(没有 servlet 匹配,只有 *.txt 的过滤器):404 “<html> <head> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/> <ti…” Content-Length=425
    事件:
        A(/*) 前(REQUEST /nothing.txt)
        E(*.txt) 前(REQUEST /nothing.txt)
        E(*.txt) 后
        A(/*) 后
  停止时:
        E(*.txt).destroy()
        D(/other).destroy()
        C(精确 /app/x).destroy()
        B(/app/*).destroy()
        A(/*).destroy()
  声明顺序对调(精确匹配的 X 先声明,通配的 Y 后声明)GET /app/x:200 “hello from hello” Content-Length=16
    事件:
        X(精确 /app/x,先声明) 前(REQUEST /app/x)
        Y(/*,后声明) 前(REQUEST /app/x)
        servlet hello 处理 /app/x 的 REQUEST
        Y(/*,后声明) 后
        X(精确 /app/x,先声明) 后

短路

实测 · 本机执行输出
  [short]
  没有 token:401 “<html> <head> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/> <ti…” Content-Length=418
    事件:
        auth 前
        auth:没有 token,不调用 chain.doFilter
  带 token:200 “hello from hello” Content-Length=16
    事件:
        auth 前
        servlet hello 处理 /x 的 REQUEST
        auth 后
  过滤器既不调用 chain.doFilter 也不写响应:200 “” Content-Length=0
    事件:
        filter:没有调用 chain.doFilter,也没有写响应
  过滤器调用 chain.doFilter 两次:200 “hello from hellohello from hello” Content-Length=32
    事件:
        filter:调用 chain.doFilter 两次
        servlet hello 处理 /x 的 REQUEST
        servlet hello 处理 /x 的 REQUEST
        第二次 chain.doFilter 没有抛异常

请求与响应的包装

实测 · 本机执行输出
  [wrap]
  请求包装(改参数 / 头 / URI):200 “name=AMY;ua=FAKE-UA;path=/p(wrapped);len=-1” Content-Length=43
  响应包装(捕获 servlet 输出再改写):200 “HELLO FROM HELLO+EXTRA” X-Test=wrapped Content-Length=22
    事件:
        servlet hello 处理 /x 的 REQUEST
        捕获到 16 字节,改写后 22 字符;写回之前 isCommitted = false
  过滤器在 chain.doFilter 之后改状态码 / 头(响应小,可能已提交):状态 500;X-Late = 1;body “hello from hello”
    事件:
        servlet hello 处理 /x 的 REQUEST
        servlet 返回之后 isCommitted = false;再 setHeader("X-Late")
        再 setStatus(500) 之后 getStatus = 500
  大响应(20 万字符,响应已提交):状态 200;X-Late = (没有);body 长度 200000
    事件:
        大响应 servlet 返回之后 isCommitted = true
        再 setStatus(500) 之后 getStatus = 200

DispatcherType

实测 · 本机执行输出
  [dispatch]
  GET /go?to=none:200 “plain” Content-Length=5
    事件:
        REQ-only 前(REQUEST /go)
        fwd servlet(REQUEST)
        REQ-only 后
  GET /go?to=forward:200 “hello from target” Content-Length=17
    事件:
        REQ-only 前(REQUEST /go)
        fwd servlet(REQUEST)
        FORWARD-only 前(FORWARD /target)
        servlet target 处理 /target 的 FORWARD
        FORWARD-only 后
        REQ-only 后
  GET /go?to=include:200 “[outer]hello from target” Content-Length=24
    事件:
        REQ-only 前(REQUEST /go)
        fwd servlet(REQUEST)
        INCLUDE-only 前(INCLUDE /go)
        servlet target 处理 /go 的 INCLUDE
        INCLUDE-only 后
        REQ-only 后
  GET /go?to=error:404 “error page” Content-Length=10
    事件:
        REQ-only 前(REQUEST /go)
        fwd servlet(REQUEST)
        REQ-only 后
        ERROR-only 前(ERROR /errpage)
        error 页面 servlet(ERROR)
        ERROR-only 后
  GET /go?to=throw:500 “error page” Content-Length=10
    事件:
        REQ-only 前(REQUEST /go)
        fwd servlet(REQUEST)
        REQ-only 后
        ERROR-only 前(ERROR /errpage)
        error 页面 servlet(ERROR)
        ERROR-only 后

异常穿过过滤器链

实测 · 本机执行输出
  [errors]
  servlet 抛异常,有个过滤器捕获并改写:503 “handled by filter” Content-Length=17
    事件:
        outer 前(REQUEST /boom)
        servlet 抛 IllegalStateException
        catching 过滤器捕获 servlet 出错,改写成 503
        outer 后
  过滤器自己抛 ServletException:500 “   <ti…” Content-Length=519
    事件:
        outer 前(REQUEST /bad)
        failing 过滤器抛 ServletException
        outer 后
  没人捕获时 servlet 抛异常:500 “ <ti…” Content-Length=520
    事件(outer 的 finally 仍然执行):
        outer 前(REQUEST /boom)
        outer 后

异步请求

实测 · 本机执行输出
  [async]
  异步请求:200 “async done” Content-Length=10;客户端实际耗时 361 ms
    事件:
        timer 过滤器 前
        servlet startAsync
        timer 过滤器 后(isAsyncStarted = true,经过 0 ms——测的不是整个请求)
        异步线程 complete() [线程 async-pool]

开销

实测 · 本机执行输出
  [cost]
   0 个空过滤器:每个请求中位数 67.7 µs(最小 66.8,最大 78.6;本机回环,串行请求,7 轮交错测量)
   1 个空过滤器:每个请求中位数 67.5 µs(最小 66.9,最大 75.9;本机回环,串行请求,7 轮交错测量)
   5 个空过滤器:每个请求中位数 67.3 µs(最小 66.9,最大 72.6;本机回环,串行请求,7 轮交错测量)
  20 个空过滤器:每个请求中位数 67.2 µs(最小 66.8,最大 69.0;本机回环,串行请求,7 轮交错测量)

分步演示

Servlet Filter
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64,JDK 21.0.12;Jetty 12.0.16(ee10,jakarta.servlet-api 6.0.0),过滤器以编程方式注册(不是 web.xml);Jetty 默认输出缓冲区约 32 KB,影响响应「是否已提交」的临界点
    • 没有验证:web.xml 中 filter-mapping 的 servlet-name 映射与 url-pattern 映射的相对顺序(这里全是 url-pattern 编程注册)、Tomcat / Undertow 的相同行为、@WebFilter 的顺序(规范未定义)、Spring 的 OncePerRequestFilter、过滤器里读取请求体后 servlet 再读的问题(未测试 getInputStream 的重复读取)、压缩 / 缓存类过滤器的 Content-Length 处理细节
    京ICP备2026067502号-1京公网安备11010502063487号