java责任链模式

Java 责任链模式:从设计取舍到可运行实战

摘要:从一段代码说起到介绍责任链

文章目录

  • [Java 责任链模式:从设计取舍到可运行实战](#Java 责任链模式:从设计取舍到可运行实战)
    • [1. 从一段"越写越乱"的校验代码说起](#1. 从一段"越写越乱"的校验代码说起)
    • [2. 模式定义与结构](#2. 模式定义与结构)
      • [2.1 三个角色](#2.1 三个角色)
      • [2.2 执行时序与短路](#2.2 执行时序与短路)
    • [3. 三种实现形态与选型](#3. 三种实现形态与选型)
      • [3.1 形态一:经典 GoF 递归版](#3.1 形态一:经典 GoF 递归版)
      • [3.2 形态二:列表迭代版(推荐)](#3.2 形态二:列表迭代版(推荐))
      • [3.3 形态三:函数式版](#3.3 形态三:函数式版)
    • [4. 完整可运行实现](#4. 完整可运行实现)
    • [5. 四个关键设计决策](#5. 四个关键设计决策)
      • [5.1 上下文(Context)怎么设计](#5.1 上下文(Context)怎么设计)
      • [5.2 顺序怎么控制](#5.2 顺序怎么控制)
      • [5.3 异常处理策略](#5.3 异常处理策略)
      • [5.4 什么时候不该用责任链](#5.4 什么时候不该用责任链)
    • [6. 实测:性能与栈深度](#6. 实测:性能与栈深度)
      • [6.1 短路到底省了多少](#6.1 短路到底省了多少)
      • [6.2 链长度对吞吐的影响](#6.2 链长度对吞吐的影响)
      • [6.3 递归版的栈深度上限(这是个真实的坑)](#6.3 递归版的栈深度上限(这是个真实的坑))
    • [7. 框架中的真实案例](#7. 框架中的真实案例)
    • [8. 与相近模式的区别](#8. 与相近模式的区别)
    • [9. 常见坑与最佳实践](#9. 常见坑与最佳实践)
    • [10. 面试高频题](#10. 面试高频题)
    • [11. 总结](#11. 总结)

1. 从一段"越写越乱"的校验代码说起

先看一段很常见的下单校验代码:

java 复制代码
public Result createOrder(OrderReq req) {
    if (req.getAmount() <= 0) {
        return Result.fail("金额非法");
    }
    if (blacklistService.contains(req.getUserId())) {
        return Result.fail("黑名单用户");
    }
    int risk = riskService.score(req);
    if (risk > 80) {
        return Result.fail("风控不通过");
    }
    if (!stockService.hasStock(req.getItemId())) {
        return Result.fail("库存不足");
    }
    if (!couponService.valid(req.getCouponCode())) {
        return Result.fail("优惠券失效");
    }
    // ...后面还有一堆 if
    return orderService.save(req);
}

这段代码能跑,但有四个问题会随着业务增长逐个爆发:

  1. 违反开闭原则:每加一条规则都要改这个方法,方法越来越长;
  2. 无法复用:某个校验想在别的场景单独用,只能复制粘贴;
  3. 顺序硬编码:想调整"先查库存还是先查风控",得改代码重新发版;
  4. 难以观测:出问题时不知道卡在哪一步,只能翻日志。

责任链模式解决的就是这件事:把每个判断拆成独立节点,串成一条链,请求沿着链流动,任一节点可以决定继续传递还是直接中断。

2. 模式定义与结构

2.1 三个角色

责任链模式的经典结构只有三个角色:

角色 职责
Handler(抽象处理者) 定义处理接口,通常持有下一个处理者的引用
ConcreteHandler(具体处理者) 实现自己的判断逻辑,决定"处理并中断"还是"交给下一个"
Client(客户端) 组装链条,把请求送入链头

#mermaid-svg-VwBmsUDf6LxVG66D{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VwBmsUDf6LxVG66D .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VwBmsUDf6LxVG66D .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VwBmsUDf6LxVG66D .error-icon{fill:#552222;}#mermaid-svg-VwBmsUDf6LxVG66D .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VwBmsUDf6LxVG66D .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VwBmsUDf6LxVG66D .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VwBmsUDf6LxVG66D .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VwBmsUDf6LxVG66D .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VwBmsUDf6LxVG66D .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VwBmsUDf6LxVG66D .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VwBmsUDf6LxVG66D .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VwBmsUDf6LxVG66D .marker.cross{stroke:#333333;}#mermaid-svg-VwBmsUDf6LxVG66D svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VwBmsUDf6LxVG66D p{margin:0;}#mermaid-svg-VwBmsUDf6LxVG66D g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-VwBmsUDf6LxVG66D g.classGroup text .title{font-weight:bolder;}#mermaid-svg-VwBmsUDf6LxVG66D .cluster-label text{fill:#333;}#mermaid-svg-VwBmsUDf6LxVG66D .cluster-label span{color:#333;}#mermaid-svg-VwBmsUDf6LxVG66D .cluster-label span p{background-color:transparent;}#mermaid-svg-VwBmsUDf6LxVG66D .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VwBmsUDf6LxVG66D .cluster text{fill:#333;}#mermaid-svg-VwBmsUDf6LxVG66D .cluster span{color:#333;}#mermaid-svg-VwBmsUDf6LxVG66D .nodeLabel,#mermaid-svg-VwBmsUDf6LxVG66D .edgeLabel{color:#131300;}#mermaid-svg-VwBmsUDf6LxVG66D .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-VwBmsUDf6LxVG66D .label text{fill:#131300;}#mermaid-svg-VwBmsUDf6LxVG66D .labelBkg{background:#ECECFF;}#mermaid-svg-VwBmsUDf6LxVG66D .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-VwBmsUDf6LxVG66D .classTitle{font-weight:bolder;}#mermaid-svg-VwBmsUDf6LxVG66D .node rect,#mermaid-svg-VwBmsUDf6LxVG66D .node circle,#mermaid-svg-VwBmsUDf6LxVG66D .node ellipse,#mermaid-svg-VwBmsUDf6LxVG66D .node polygon,#mermaid-svg-VwBmsUDf6LxVG66D .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VwBmsUDf6LxVG66D .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-VwBmsUDf6LxVG66D g.clickable{cursor:pointer;}#mermaid-svg-VwBmsUDf6LxVG66D g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-VwBmsUDf6LxVG66D g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-VwBmsUDf6LxVG66D .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-VwBmsUDf6LxVG66D .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-VwBmsUDf6LxVG66D .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-VwBmsUDf6LxVG66D .dashed-line{stroke-dasharray:3;}#mermaid-svg-VwBmsUDf6LxVG66D .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-VwBmsUDf6LxVG66D #compositionStart,#mermaid-svg-VwBmsUDf6LxVG66D .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-VwBmsUDf6LxVG66D #compositionEnd,#mermaid-svg-VwBmsUDf6LxVG66D .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-VwBmsUDf6LxVG66D #dependencyStart,#mermaid-svg-VwBmsUDf6LxVG66D .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-VwBmsUDf6LxVG66D #dependencyStart,#mermaid-svg-VwBmsUDf6LxVG66D .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-VwBmsUDf6LxVG66D #extensionStart,#mermaid-svg-VwBmsUDf6LxVG66D .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-VwBmsUDf6LxVG66D #extensionEnd,#mermaid-svg-VwBmsUDf6LxVG66D .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-VwBmsUDf6LxVG66D #aggregationStart,#mermaid-svg-VwBmsUDf6LxVG66D .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-VwBmsUDf6LxVG66D #aggregationEnd,#mermaid-svg-VwBmsUDf6LxVG66D .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-VwBmsUDf6LxVG66D #lollipopStart,#mermaid-svg-VwBmsUDf6LxVG66D .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-VwBmsUDf6LxVG66D #lollipopEnd,#mermaid-svg-VwBmsUDf6LxVG66D .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-VwBmsUDf6LxVG66D .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-VwBmsUDf6LxVG66D .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VwBmsUDf6LxVG66D .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VwBmsUDf6LxVG66D .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VwBmsUDf6LxVG66D :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 聚合多个节点
OrderHandler
+name() : String
+handle(ctx) : boolean
ParamValidateHandler
+handle(ctx) : boolean
BlacklistHandler
+handle(ctx) : boolean
RiskScoreHandler
+handle(ctx) : boolean
OrderChain
-List handlers
+add(h) : OrderChain
+execute(ctx) : ChainResult

2.2 执行时序与短路

关键行为是短路(short-circuit):节点返回"不通过"时,链立即中断,后续节点不再执行。
Stock RiskScore Blacklist ParamValidate OrderChain Client Stock RiskScore Blacklist ParamValidate OrderChain Client #mermaid-svg-syR53ZSSeuACTWo3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-syR53ZSSeuACTWo3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-syR53ZSSeuACTWo3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-syR53ZSSeuACTWo3 .error-icon{fill:#552222;}#mermaid-svg-syR53ZSSeuACTWo3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-syR53ZSSeuACTWo3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-syR53ZSSeuACTWo3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-syR53ZSSeuACTWo3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-syR53ZSSeuACTWo3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-syR53ZSSeuACTWo3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-syR53ZSSeuACTWo3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-syR53ZSSeuACTWo3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-syR53ZSSeuACTWo3 .marker.cross{stroke:#333333;}#mermaid-svg-syR53ZSSeuACTWo3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-syR53ZSSeuACTWo3 p{margin:0;}#mermaid-svg-syR53ZSSeuACTWo3 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-syR53ZSSeuACTWo3 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-syR53ZSSeuACTWo3 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-syR53ZSSeuACTWo3 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-syR53ZSSeuACTWo3 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-syR53ZSSeuACTWo3 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-syR53ZSSeuACTWo3 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-syR53ZSSeuACTWo3 .sequenceNumber{fill:white;}#mermaid-svg-syR53ZSSeuACTWo3 #sequencenumber{fill:#333;}#mermaid-svg-syR53ZSSeuACTWo3 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-syR53ZSSeuACTWo3 .messageText{fill:#333;stroke:none;}#mermaid-svg-syR53ZSSeuACTWo3 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-syR53ZSSeuACTWo3 .labelText,#mermaid-svg-syR53ZSSeuACTWo3 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-syR53ZSSeuACTWo3 .loopText,#mermaid-svg-syR53ZSSeuACTWo3 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-syR53ZSSeuACTWo3 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-syR53ZSSeuACTWo3 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-syR53ZSSeuACTWo3 .noteText,#mermaid-svg-syR53ZSSeuACTWo3 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-syR53ZSSeuACTWo3 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-syR53ZSSeuACTWo3 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-syR53ZSSeuACTWo3 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-syR53ZSSeuACTWo3 .actorPopupMenu{position:absolute;}#mermaid-svg-syR53ZSSeuACTWo3 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-syR53ZSSeuACTWo3 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-syR53ZSSeuACTWo3 .actor-man circle,#mermaid-svg-syR53ZSSeuACTWo3 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-syR53ZSSeuACTWo3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 短路:H3/H4 不再执行 execute(ctx) handle(ctx) true handle(ctx) false(拦截) BLOCKED by Blacklist

3. 三种实现形态与选型

这是本文最核心的选型表,三种形态各有适用场景:

形态 驱动方式 优点 缺点 典型应用
经典 GoF 链表递归 next.handle() 递归 结构最贴近定义,节点自己决定后继 占用栈帧,长链会 StackOverflow;无法统一编排 Netty ChannelPipeline
列表迭代(Spring 风格) for 循环 + 有序 List 无栈深度风险;顺序可配置;易统一加日志/监控 需要额外的链容器 Spring HandlerInterceptor、Servlet Filter
函数式组合 Predicate.and() 代码极简,无额外类 难做复杂短路语义与链路追踪;调试困难 简单校验场景

我的建议 :生产环境优先选列表迭代版。递归版那个看似遥远的风险是真实存在的------本文第 6 节实测出了具体数字。

3.1 形态一:经典 GoF 递归版

java 复制代码
public abstract class AbstractHandler {
    private AbstractHandler next;

    public AbstractHandler setNext(AbstractHandler h) {
        this.next = h;
        return h;                       // 返回入参,方便链式书写
    }

    public final boolean handle(OrderContext ctx) {
        if (!doHandle(ctx)) {
            return false;               // 短路:不再向后传递
        }
        if (next == null) {
            return true;
        }
        return next.handle(ctx);        // 递归调用
    }

    protected abstract boolean doHandle(OrderContext ctx);
    protected abstract String name();
}

注意 handle()final 修饰、doHandle() 交给子类------这是模板方法模式的经典配合,保证"传递"逻辑不被子类破坏。

3.2 形态二:列表迭代版(推荐)

java 复制代码
public interface OrderHandler {
    String name();

    /** 返回 false 表示拦截并中断链路 */
    boolean handle(OrderContext ctx);
}

public class OrderChain {
    private final List<OrderHandler> handlers = new ArrayList<>();

    public OrderChain add(OrderHandler h) {
        handlers.add(h);
        return this;
    }

    public ChainResult execute(OrderContext ctx) {
        for (int i = 0; i < handlers.size(); i++) {
            OrderHandler h = handlers.get(i);
            ctx.getTrail().add(h.name());          // 链路追踪
            if (!h.handle(ctx)) {
                return ChainResult.blocked(h.name(), i + 1);
            }
        }
        return ChainResult.passed(handlers.size());
    }
}

3.3 形态三:函数式版

java 复制代码
Predicate<OrderContext> chain = ctx -> true;
chain = chain.and(ctx -> ctx.getAmount() > 0);
chain = chain.and(ctx -> !ctx.isBlacklisted());
chain = chain.and(ctx -> ctx.isStockOk());

boolean ok = chain.test(ctx);

简洁,但拿不到"被哪个节点拦截"的信息,链路追踪也无从下手。适合快速校验,不适合需要可观测性的生产链路。

4. 完整可运行实现

下面是一份可直接复制运行的最小实现(Java 8 兼容)。

java 复制代码
import java.util.*;

public class ChainDemo {

    /** 上下文:链上传递的可变状态 */
    static class OrderContext {
        final String orderId;
        final int amount;
        boolean blacklisted;
        boolean stockOk = true;
        final List<String> trail = new ArrayList<>();   // 链路追踪

        OrderContext(String orderId, int amount) {
            this.orderId = orderId;
            this.amount = amount;
        }
    }

    /** 链路结果 */
    static class ChainResult {
        final boolean pass;
        final String blockedBy;
        final int executedNodes;

        ChainResult(boolean pass, String blockedBy, int executedNodes) {
            this.pass = pass;
            this.blockedBy = blockedBy;
            this.executedNodes = executedNodes;
        }
    }

    interface OrderHandler {
        String name();

        boolean handle(OrderContext ctx);
    }

    static class OrderChain {
        private final List<OrderHandler> handlers = new ArrayList<>();

        OrderChain add(OrderHandler h) {
            handlers.add(h);
            return this;
        }

        ChainResult execute(OrderContext ctx) {
            for (int i = 0; i < handlers.size(); i++) {
                OrderHandler h = handlers.get(i);
                ctx.trail.add(h.name());
                if (!h.handle(ctx)) {
                    return new ChainResult(false, h.name(), i + 1);
                }
            }
            return new ChainResult(true, null, handlers.size());
        }
    }

    public static void main(String[] args) {
        OrderChain chain = new OrderChain();
        chain.add(new OrderHandler() {
            public String name() { return "ParamValidate"; }
            public boolean handle(OrderContext c) { return c.amount > 0; }
        });
        chain.add(new OrderHandler() {
            public String name() { return "Blacklist"; }
            public boolean handle(OrderContext c) { return !c.blacklisted; }
        });
        chain.add(new OrderHandler() {
            public String name() { return "Stock"; }
            public boolean handle(OrderContext c) { return c.stockOk; }
        });

        OrderContext bad = new OrderContext("SO002", 19900);
        bad.blacklisted = true;

        ChainResult r = chain.execute(bad);
        System.out.println("pass=" + r.pass
                + " blockedBy=" + r.blockedBy
                + " trail=" + bad.trail);
    }
}

运行结果:

复制代码
pass=false blockedBy=Blacklist trail=[ParamValidate, Blacklist]

trail 只到 Blacklist,证明 Stock 节点根本没有执行------这就是短路生效的直接证据。

5. 四个关键设计决策

5.1 上下文(Context)怎么设计

上下文是链上唯一的共享状态,设计上有个常见分歧:

方案 做法 适用
可变上下文 各节点读写同一个 OrderContext 节点间需要传递中间结果(如风控分)
不可变上下文 每步返回新对象 需要并发安全、可回溯

实践建议 :用可变上下文,但把"输入参数"和"计算产物"分开 。输入用 final 字段,产物用普通字段,避免节点误改上游数据。

5.2 顺序怎么控制

顺序错了会出事------比如"优惠券核销"放在"落库"之后,一旦落库失败,券已经用了。

方式 做法 说明
显式注册顺序 chain.add(a).add(b) 最直观,但散落在代码里
注解排序 @Order(1) + 启动时排序 Spring 常用,但顺序不直观
配置化 顺序写在配置中心 可动态调整,需防配置错误

5.3 异常处理策略

节点抛异常时,链路该怎么办?三种策略:
#mermaid-svg-xxfsPaxhZyVb5wVk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xxfsPaxhZyVb5wVk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xxfsPaxhZyVb5wVk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xxfsPaxhZyVb5wVk .error-icon{fill:#552222;}#mermaid-svg-xxfsPaxhZyVb5wVk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xxfsPaxhZyVb5wVk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xxfsPaxhZyVb5wVk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xxfsPaxhZyVb5wVk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xxfsPaxhZyVb5wVk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xxfsPaxhZyVb5wVk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xxfsPaxhZyVb5wVk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xxfsPaxhZyVb5wVk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xxfsPaxhZyVb5wVk .marker.cross{stroke:#333333;}#mermaid-svg-xxfsPaxhZyVb5wVk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xxfsPaxhZyVb5wVk p{margin:0;}#mermaid-svg-xxfsPaxhZyVb5wVk .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xxfsPaxhZyVb5wVk .cluster-label text{fill:#333;}#mermaid-svg-xxfsPaxhZyVb5wVk .cluster-label span{color:#333;}#mermaid-svg-xxfsPaxhZyVb5wVk .cluster-label span p{background-color:transparent;}#mermaid-svg-xxfsPaxhZyVb5wVk .label text,#mermaid-svg-xxfsPaxhZyVb5wVk span{fill:#333;color:#333;}#mermaid-svg-xxfsPaxhZyVb5wVk .node rect,#mermaid-svg-xxfsPaxhZyVb5wVk .node circle,#mermaid-svg-xxfsPaxhZyVb5wVk .node ellipse,#mermaid-svg-xxfsPaxhZyVb5wVk .node polygon,#mermaid-svg-xxfsPaxhZyVb5wVk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xxfsPaxhZyVb5wVk .rough-node .label text,#mermaid-svg-xxfsPaxhZyVb5wVk .node .label text,#mermaid-svg-xxfsPaxhZyVb5wVk .image-shape .label,#mermaid-svg-xxfsPaxhZyVb5wVk .icon-shape .label{text-anchor:middle;}#mermaid-svg-xxfsPaxhZyVb5wVk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xxfsPaxhZyVb5wVk .rough-node .label,#mermaid-svg-xxfsPaxhZyVb5wVk .node .label,#mermaid-svg-xxfsPaxhZyVb5wVk .image-shape .label,#mermaid-svg-xxfsPaxhZyVb5wVk .icon-shape .label{text-align:center;}#mermaid-svg-xxfsPaxhZyVb5wVk .node.clickable{cursor:pointer;}#mermaid-svg-xxfsPaxhZyVb5wVk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xxfsPaxhZyVb5wVk .arrowheadPath{fill:#333333;}#mermaid-svg-xxfsPaxhZyVb5wVk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xxfsPaxhZyVb5wVk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xxfsPaxhZyVb5wVk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xxfsPaxhZyVb5wVk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xxfsPaxhZyVb5wVk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xxfsPaxhZyVb5wVk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xxfsPaxhZyVb5wVk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xxfsPaxhZyVb5wVk .cluster text{fill:#333;}#mermaid-svg-xxfsPaxhZyVb5wVk .cluster span{color:#333;}#mermaid-svg-xxfsPaxhZyVb5wVk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xxfsPaxhZyVb5wVk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xxfsPaxhZyVb5wVk rect.text{fill:none;stroke-width:0;}#mermaid-svg-xxfsPaxhZyVb5wVk .icon-shape,#mermaid-svg-xxfsPaxhZyVb5wVk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xxfsPaxhZyVb5wVk .icon-shape p,#mermaid-svg-xxfsPaxhZyVb5wVk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xxfsPaxhZyVb5wVk .icon-shape .label rect,#mermaid-svg-xxfsPaxhZyVb5wVk .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xxfsPaxhZyVb5wVk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xxfsPaxhZyVb5wVk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xxfsPaxhZyVb5wVk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 核心强校验

如风控
非核心

如打点、通知
可降级

如优惠券
节点抛出异常
节点性质
Fail-Fast

直接中断并上报
Fail-Silent

捕获后记日志继续
Fail-Over

降级为默认结果继续
返回明确失败原因
链路继续

原则:能明确判断的异常必须中断(用户看到的是"风控拒绝"而不是"系统错误");不影响主流程的旁路节点必须吞掉异常,否则一个埋点失败会拖垮整个下单。

5.4 什么时候不该用责任链

  • 节点之间强依赖执行顺序且有大量数据耦合------这时用流水线(Pipeline)更合适;
  • 只有 2-3 个判断且永远不会变------直接写 if 更清晰,别过度设计;
  • 需要并行执行以提高性能------责任链本质是串行模型。

6. 实测:性能与栈深度

以上都是本地真实执行的结果,环境为 Java 1.8.0_101 / HotSpot 64-Bit Server VM

6.1 短路到底省了多少

12 节点链,对比"全部通过"与"第 2 个节点失败":

复制代码
full pass (12 nodes) :    341.4 ns/call
fail at node 2       :     88.4 ns/call
saved by short-circuit: 74.1%

短路节省了 74.1% 的耗时。 这说明一个重要的工程结论:把高失败率的节点尽量前置(比如参数校验、黑名单),能让绝大多数非法请求在链路早期就被拦下,整体吞吐显著提升。

6.2 链长度对吞吐的影响

节点数 吞吐(ops/sec) 单次耗时(ns) 单节点均摊(ns)
3 5,786,454 172.8 57.6
6 3,794,197 263.6 43.9
12 2,476,519 403.8 33.6
24 1,335,526 748.8 31.2

有个值得注意的现象:单节点均摊成本随链变长而下降(57.6 → 31.2 ns)。这说明存在一笔固定开销。

用 12 与 24 节点的数据做差分,可以估算:

t 节点 = 748.8 − 403.8 24 − 12 ≈ 28.8 ns/节点 t_{节点} = \frac{748.8 - 403.8}{24 - 12} \approx 28.8\ \text{ns/节点} t节点=24−12748.8−403.8≈28.8 ns/节点

t 固定 = 403.8 − 12 × 28.8 ≈ 58 ns t_{固定} = 403.8 - 12 \times 28.8 \approx 58\ \text{ns} t固定=403.8−12×28.8≈58 ns

即:每次调用约 58 ns 固定开销(上下文对象创建 + 链路追踪容器),加上每节点约 29 ns 的边际成本

工程含义很直接:如果你的节点逻辑本身很轻(几十纳秒级),那么"建上下文"的固定开销可能比节点逻辑还贵。这时优化方向不是精简节点,而是复用上下文对象、或去掉不必要的 trail 记录。

6.3 递归版的栈深度上限(这是个真实的坑)

经典 GoF 递归版每个节点占一个栈帧。实测(默认 -Xss):

复制代码
  depth 1000     -> OK
  depth 10000    -> OK
  depth 20000    -> OK
  depth 50000    -> StackOverflowError
  depth 100000   -> StackOverflowError

  >>> 递归版最大安全链长 ≈ 49970 个节点
  >>> 列表迭代版无此上限(受内存而非栈限制)

两万以内很安全,五万必炸。实际业务链很少超过几十个节点,所以这个风险平时不会暴露------但一旦有人把链做成了动态配置、或者节点里又嵌套了子链,递归版就成了定时炸弹

这也是我推荐列表迭代版的根本原因:它把"链长"的约束从栈大小 变成了内存大小,后者宽松得多。

7. 框架中的真实案例

责任链在主流框架里到处都是,认清它们有助于建立直觉:

框架 实现 形态
Servlet Filter FilterChain.doFilter() 递归 / 迭代混合,支持前置+后置(环绕)
Spring HandlerInterceptor preHandle / postHandle / afterCompletion 列表迭代 ,顺序由 order 控制
Netty ChannelPipeline ChannelInboundHandler 双向链 链表递归,inbound 与 outbound 分离
MyBatis Plugin Interceptor + 动态代理 层层代理,本质是责任链变体
Sentinel Slot Chain ProcessorSlotChain 列表迭代 ,用 @SpiOrder 控制顺序

值得注意的是 Spring Interceptor 提供了 preHandle / postHandle / afterCompletion 三段 ,这其实是责任链 + 环绕通知的组合:前置拦截可以短路,后置处理则按逆序执行(类似栈的展开)。如果你的业务需要"资源申请---释放"这种配对语义,可以参考这个设计。

8. 与相近模式的区别

面试高频,也最容易搞混:

模式 核心区别 判断口诀
责任链 多个处理者依次尝试,谁行谁上,可中断 somebody handles it
装饰器 层层叠加,每个都要执行,增强功能 all of them run
策略 多个算法选一个,客户端主动选择 pick one
命令 把请求封装成对象,支持排队/撤销 encapsulate request
观察者 一对多广播,发布者不关心谁处理 broadcast

最容易混淆的是责任链 vs 装饰器 :两者都是"链式",区别在于------责任链中任一节点可以中断 链路,装饰器中每个装饰器必须调用下一个(否则功能缺失)。

9. 常见坑与最佳实践

表现 解法
链断裂 节点忘记调用下一个,请求石沉大海 用列表迭代版,把"传递"交给框架而非节点
顺序隐式 靠注册顺序隐式决定,改一处全崩 顺序显式化,加启动自检
上下文污染 节点误改上游字段,后续节点行为异常 输入字段用 final,产物字段单独分区
异常吞噬 节点捕获异常后不记录,排查无门 统一在链层捕获并记日志,节点只抛不吞
链过长 几十个节点,单次调用耗时不可控 拆分子链,或按场景分组
节点有状态 单例节点持有可变字段,并发下串数据 节点设计成无状态,状态放上下文

最佳实践清单

  1. 节点无状态,所有状态放上下文;
  2. 高失败率节点前置,借助短路提升吞吐(实测可省 74%);
  3. 链路必须留痕(trail),否则线上排障全靠猜;
  4. 生产用列表迭代版,避开递归栈风险;
  5. 链的定义与节点的注册分离,方便测试单个节点。

10. 面试高频题

  1. 责任链和装饰器的区别? 关键在能否中断------责任链可短路,装饰器必须传递。
  2. 经典递归版有什么隐患? 栈深度限制,本文实测约 5 万节点溢出;长链或嵌套子链时风险真实存在。
  3. 如何保证顺序? 显式注册 / @Order 注解排序 / 配置化,三者权衡。
  4. 上下文设计要注意什么? 输入用 final 防污染,产物单独分区,节点保持无状态。
  5. Spring Interceptor 为什么有 pre/post/after 三段? 支持环绕语义,后置按逆序执行,便于资源释放。
  6. 怎么提升责任链性能? 高失败率节点前置以利用短路;复用上下文减少固定开销(实测固定开销约 58 ns)。

11. 总结

责任链模式本身不复杂,复杂的是选型和边界

三种形态里,列表迭代版是生产环境的默认选择 ------它没有递归版的栈深度风险,顺序可配置,还便于在链层统一加日志、监控与异常处理。本文实测递归版在约 5 万节点时就会 StackOverflowError,虽然日常业务碰不到,但这个约束是真实存在的。

性能上有个反直觉的发现:每次调用的固定开销约 58 ns,而单节点边际成本只有约 29 ns。这意味着当节点逻辑很轻时,优化重点应该是上下文的创建与链路追踪的开销,而不是一味精简节点数量。

最后,短路是责任链最有价值的特性------实测能省下 74.1% 的耗时。所以把最可能失败的校验放在链的最前面,这是投入产出比最高的一条优化。

如果本文对你有帮助,欢迎点赞收藏。示例代码可直接复制运行,建议改改节点顺序和链长,亲眼看看数据变化。

相关推荐
AI深栈1 小时前
第 10 章 · Embedding、VectorStore 与 RAG
java·人工智能
Java内核笔记2 小时前
Spring Boot 4 SSRF 防护源码剖析:InetAddressFilter 挡住内网地址与云元数据
java·后端
白远山2 小时前
健身场馆无人自动化系统实战指南:从架构设计到部署全流程解析
java·开发语言·架构·需求分析
user_admin_god2 小时前
第 09 篇:实践一 —— 文本摘要(Map-Reduce 分块)
java·人工智能·spring boot·语言模型
咖啡八杯2 小时前
Controller 基类封装:BaseController 的模板方法设计
java·架构·设计
vx_BS813302 小时前
【项目编号:project49759】Spring Boot 宠物综合服务平台:商城、预约、咨询、活动与健康档案的一体化实现
java·spring boot·eclipse·tomcat·intellij-idea
她的男孩2 小时前
加了 @Idempotent 还是重复扣款了 3 笔:扒完 1279 行幂等 Starter,我挖出 5 个隐蔽的坑
java·后端·架构
步行cgn2 小时前
Spring 注入内部 Bean 详解
java·spring·rpc
user_admin_god2 小时前
第 05 篇:结构化输出 —— 让模型稳定返回 JSON
java·人工智能·spring boot·语言模型