Skip to content
Go back

责任链模式——从Servlet FilterChain到Spring Interceptor

责任链模式:Servlet Filter 和 Spring Interceptor 的实现对比

一句话结论(30s)

责任链模式的本质,是把「堆在一个方法里的一串处理/校验步骤」拆成可独立复用、按顺序串联、失败可逆序补偿的节点。我在个人项目「吃什么」本地生活点评平台里用它重构了消息发送链路——因为消息类型从 2 种扩展到 7 种后,所有前置校验堆进一个方法变成箭头型代码、圈复杂度冲到 23,改一个场景要动全局;重构后每个校验规则独立成一个 Handler,圈复杂度从 23 降到单节点 ≤3、整体约降 40%,单元测试覆盖率从 34% 提升到 92%。

背景诉求

目标边界

不做

核心难点

  1. 节点怎么串联:不用递归(深链路有栈溢出风险),用迭代 + 索引(pos / interceptorIndex)驱动方法重入,链长安全无上限。
  2. 失败怎么处理:中间某节点失败后,后面的节点不再执行;关键是「已经执行过的节点怎么撤」——必须逆序补偿,让「做得越多、撤得越彻底」,与执行顺序对称。
  3. 顺序怎么保证:链的顺序即业务校验的先后(如先鉴权、再限流、再参数校验),顺序错了会导致依赖错误——后置步骤依赖前置步骤的产物。
  4. 补偿与主流程的对称性:每个 Handler 都要成对设计 handle()/compensate(),缺了补偿,异常路径就会漏清理,出现「回滚了一半」。

思考穿插:为什么链执行偏要用「迭代 + 索引」,而不直接递归? 递归的本质是「每个节点调用下一个节点」,节点不返回、栈帧就不弹,链一长调用栈里就压着一串未返回的栈帧,几十上百个节点可能先 StackOverflow。迭代 + 索引是把「推进到哪了」这个状态从调用栈搬到堆上的一个游标变量(pos / interceptorIndex),栈帧始终只有固定几层,链长就没有上限——这正是 Tomcat 放弃递归、改用迭代写 FilterChain 的底层原因。

关键取舍

责任链 vs 策略 vs 管道:

思考穿插:责任链和策略模式到底什么区别? 一句判断锚点——调用方是要「选一个」还是「串一串」?策略解决「同一动作的 N 种算法里选一个执行」,一次只跑一个;责任链解决「N 个步骤按顺序都要过一遍、任一失败即停」,节点都会被依次触发。所以校验场景是「都要过」,天然该选责任链;而「支付方式 N 选 1」才是策略的主场。

个人行动

结果与复盘

结果(数据)

复盘(学到什么)

三版本回答

30 秒版(一句话)

我用责任链模式重构了「吃什么」平台的消息发送链路——因为消息类型从 2 种扩展到 7 种后,前置校验堆成一个箭头型方法、圈复杂度冲到 23;重构后拆成独立 Handler 顺序串联、失败逆序补偿,圈复杂度降到单节点 ≤3、整体约降 40%,单测覆盖率从 34% 提到 92%。

2 分钟版(背景 → 难点 → 方案 → 结果)

背景:消息发送从 2 种类型扩到 7 种,每种校验不同,全堆一个方法,箭头型代码、耦合高、改一处动全局,圈复杂度 23。

难点:一是节点怎么串联,深链路用递归会栈溢出;二是中间节点失败,已执行过的节点怎么对称清理;三是顺序怎么保证。

方案:抽象 MessageHandler(handle + compensate),用迭代 + 游标驱动链执行,失败 break 后逆序 compensate,做到「做得越多、撤得越彻底」——这是从 Tomcat FilterChain(迭代 + pos 索引)和 Spring Interceptor(逆序 afterCompletion)借鉴的。

结果:圈复杂度 23 → 单节点 ≤3、整体约降 40%,单测覆盖率 34% → 92%,新增消息类型只加 Handler、不改旧代码。

5-10 分钟版(深度展开)


底层深入(技术细节)

纯 vs 不纯责任链

源码说明:Servlet 不在 JDK 里

javax.servlet.Filter / FilterChain 不属于 JDK——它们是 Jakarta EE 的 Servlet API(由 Tomcat / Jetty 提供),不在本仓库 library/jdk 源码树里。所以这里用两处真实存在于源码树的等价实现来讲链式调用与逆序补偿:

JDK 的 Filter + Chain:doFilter 链式调用

public abstract class Filter {

    public static class Chain {
        private ListIterator<Filter> iter;
        private HttpHandler handler;

        public Chain(List<Filter> filters, HttpHandler handler) {
            iter = filters.listIterator();
            this.handler = handler;
        }

        public void doFilter(HttpExchange exchange) throws IOException {
            if (!iter.hasNext()) {
                handler.handle(exchange);
            } else {
                Filter f = iter.next();
                f.doFilter(exchange, this);
            }
        }
    }

    public abstract void doFilter(HttpExchange exchange, Chain chain)
        throws IOException;
}

这就是「Filter 接口 + FilterChain 的 doFilter 链式调用」的原始结构,与 Servlet 的 Filter.doFilter(req, res, chain) / FilterChain.doFilter(req, res) 一一对应:Filter 只有一个抽象方法 doFilter(exchange, chain),处理器自己在方法体里决定「调 chain.doFilter(exchange) 放行」还是「不调、直接终止」。Chain 内部用 ListIterator 当游标,iter.next() 取出下一个 Filter 并把 this(链本身)传进去,游标耗尽(!iter.hasNext())时落到 handler.handle(exchange)——链尾就是真正的目标。

它同样是迭代驱动、而非显式递归f.doFilter(exchange, this) 里的 this 是同一个 Chain,靠 iter 游标前进;每个 Filter 的 doFilter 栈帧要等 chain.doFilter 返回后才弹出,所以「doFilter 之前的前置代码」和「doFilter 之后的后置代码」天然对称。深链路下栈帧随 Filter 数量线性增长,这是 Servlet/Filter 链的通用形态。

Spring HandlerInterceptor:逆序 afterCompletion

真实接口(HandlerInterceptor.java)——三个钩子全是 default 方法,preHandle 默认放行、后两个默认空实现:

public interface HandlerInterceptor {

    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        return true;
    }

    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable ModelAndView modelAndView) throws Exception {
    }

    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable Exception ex) throws Exception {
    }
}

真实链执行器(HandlerExecutionChain.java)——interceptorIndex 记录「最后一个 preHandle 成功」的下标,失败即逆序补偿:

    private final List<HandlerInterceptor> interceptorList = new ArrayList<>();

    private int interceptorIndex = -1;

    boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        for (int i = 0; i < this.interceptorList.size(); i++) {
            HandlerInterceptor interceptor = this.interceptorList.get(i);
            if (!interceptor.preHandle(request, response, this.handler)) {
                triggerAfterCompletion(request, response, null);
                return false;
            }
            this.interceptorIndex = i;
        }
        return true;
    }

    void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
        for (int i = this.interceptorIndex; i >= 0; i--) {
            HandlerInterceptor interceptor = this.interceptorList.get(i);
            try {
                interceptor.afterCompletion(request, response, this.handler, ex);
            }
            catch (Throwable ex2) {
                logger.error("HandlerInterceptor.afterCompletion threw exception in interceptor [" + interceptor + "]", ex2);
            }
        }
    }

逆序补偿的精髓在 interceptorIndex:正常路径 applyPreHandle 每过一个节点就 interceptorIndex = i 前进一位;一旦某个 preHandle 返回 falsetriggerAfterCompletioninterceptorIndex 往回(i--)调 afterCompletion——只补偿「preHandle 已成功返回 true」的那些节点。Interceptor A、B、C 都成功、C 返回 false 时,interceptorIndex 停在 B(下标 1),于是逆序 B→A 补偿,而 C 自己没成功执行过 preHandle、不被补偿。

逆序补偿的理由:A 先执行 preHandle(做了预操作,如写审计日志),B 后执行。若失败时先清 A 再清 B——B 的预操作还在、A 已被「回滚」,不对称。逆序(后进先出的栈序)保证「做得越多、撤得越彻底」,与执行顺序完全对称。真实实现里 afterCompletion 的异常被 catch (Throwable) 兜底并 logger.error,不会因为某个节点的清理失败就中断整条补偿链——这也是生产级责任链的关键细节。

项目实战:消息发送责任链

在 WhatToEat 项目中,消息发送从 2 种扩展到 7 种类型。每种消息的校验逻辑不同,所有校验堆在一个方法里,圈复杂度达到 23。

重构为责任链:每个校验规则独立为一个 Handler:

abstract class MessageHandler {
    abstract boolean handle(MessageContext ctx);
    // 失败补偿
    void compensate(MessageContext ctx) { }
}

// 链执行
boolean executeChain(List<MessageHandler> handlers, MessageContext ctx) {
    int executed = 0;
    for (; executed < handlers.size(); executed++) {
        if (!handlers.get(executed).handle(ctx)) break;
    }
    // 逆序补偿
    for (int i = executed - 1; i >= 0; i--) {
        handlers.get(i).compensate(ctx);
    }
    return executed == handlers.size();
}

圈复杂度从 23 → 每个 Handler ≤ 3,单元测试覆盖率从 34% → 92%。

总结

实现链结构失败处理
Tomcat Filter迭代 + pos 索引直接返回,不调后续 Filter
Spring Interceptor迭代 + interceptorIndex逆序 afterCompletion
Netty Pipeline双向链表(头→尾入站,尾→头出站)异常沿链传递

章末提问

Q1:责任链和策略模式的区别? 结论先行:责任链解决「一串步骤按顺序叠加、任一失败即停」,策略解决「同一动作多种算法 N 选 1」。因为责任链里每个节点都会被依次触发、各自决定放行或终止,而策略一次只执行被选中的那一个算法,两者解决的维度完全不同。

Q2:为什么链执行不用递归,而用迭代 + 索引? 结论先行:为了把「推进到哪了」的状态从调用栈搬到堆上的游标变量,避免深链路栈溢出。因为递归时每个节点都要等下一个节点返回才弹栈,链越长栈帧压得越深;迭代 + pos/interceptorIndex 只占一个变量的空间,方法栈帧固定,链长无上限。

Q3:逆序补偿为什么必须逆序,正序清理错在哪? 结论先行:因为补偿必须与执行顺序严格对称(后进先出),否则会出现「回滚了一半」。A 先做预操作、B 后做,若失败时正序先清 A,B 的预操作还残留着,状态不对称;逆序 B→A 才能做到「做得越多、撤得越彻底」。

Q4:Spring 的 preHandle 返回 false 时,那个失败的 interceptor 自己会被补偿吗? 结论先行:不会,因为 interceptorIndex 只记录「最后一个 preHandle 成功」的下标。失败的节点没有成功执行过 preHandle,预操作并不存在,自然无需补偿;triggerAfterCompletion 从 interceptorIndex 往回,恰好只补偿已成功的那一批节点。

Q5:什么场景不适合硬上责任链? 结论先行:节点少、顺序固定、又没有补偿需求的场景,硬上责任链反而徒增抽象成本。因为责任链的价值在于「摊薄复杂度 + 逆序补偿」,只有两三种消息、三五步校验时,一个顺序方法更直观,抽象基类和执行器的成本反而高于收益。


Share this post on:

Previous Post
brk与mmap详解:malloc底层内存分配的双引擎
Next Post
观察者模式——Spring事件驱动架构的基石