做后端开发的人,基本都遇到过这类代码:一个入口方法里,十几个if按顺序堆下去,每个if都在做“校验 → 处理 → 决定是否继续”。刚开始只有两三个分支还好,等到业务跑起来,新需求不断加进来,这个方法的长度越来越不可控,改起来也越来越小心,因为你根本说不清当前这段逻辑到底能不能直接return。
很多人第一反应是“抽方法、拆类”,这当然没错。但更关键的问题其实不是代码行数,而是职责边界:谁来处理这个请求、按什么顺序处理、处理完之后是否继续传递。这套组织方式,正是责任链模式要解决的核心问题。
我的判断是:责任链模式不是“用来消除 if-else 的语法糖”,它真正改变的是处理逻辑的耦合方向——从客户端代码里“顺序调用一堆对象”,变成“每个对象自己决定是否接管、是否继续传递”。读完这篇文章,你会搞清楚责任链模式的适用边界、三种常见工程实现、与 Spring 容器的整合方式,以及几个真正容易踩的坑。
1. 责任链模式真正解决的是什么问题
责任链模式是 GoF 23 种设计模式之一,属于行为型模式。它的官方定义看起来非常简洁:避免请求发送者与接收者耦合,让多个接收对象都有机会处理请求,并将这些对象连成一条链,沿着链传递请求,直到有对象处理它为止。
但官方定义容易让人产生一个误解:责任链就是“多个处理类排好队,按顺序执行”。如果只是这么理解,你完全可以用一个List加for循环实现,根本不需要什么模式。责任链真正的价值,藏在下面这三点里。
第一,请求方不需要知道最终是谁处理了自己。调用者只面向一个抽象入口,至于这个请求是被第一个处理器拦截、被中间某个处理器终结,还是走完整个链到达兜底逻辑,调用者完全无感。
第二,处理者之间的关系由运行时决定。同一套处理器,今天可以按 A → B → C 的顺序执行,明天可以变成 A → C → B,或者动态插入一个新的处理器 D,这些变化不需要修改请求方代码。
第三,每个处理者都能独立决定“自己处理”还是“交给下一个”。这一点和普通列表遍历有本质区别。普通遍历里,每个元素都会被访问到;而责任链里,任何一个节点都可以选择不再向后传递,让整个处理流程提前终止。
在我接触过的项目里,责任链最典型的应用场景有三个:审批流、框架过滤器、规则校验链。这三种场景有一个共同特征:处理节点会随着业务发展不断扩展,且每个节点对请求的态度不是统一的“必须要做”,而是“先判断是否归我管”。
2. 责任链模式核心概念
2.1 三个核心角色
责任链模式通常由三个角色组成。
抽象处理者(Handler):定义处理请求的接口,一般会包含一个设置下一个处理者的方法,以及一个处理请求的抽象方法。在 Java 里,它往往被定义成抽象类或接口。
具体处理者(ConcreteHandler):实现抽象处理者的具体类。每个具体处理者会在自己的handle方法里做两件事:先判断自己能否处理当前请求,能处理就处理,不能处理就调用下一个处理者。
客户端(Client):负责组装责任链,并向链头发起请求。注意,客户端只需要持有链头对象,不需要知道整个链上有哪些处理者。
这三个角色设计得非常巧妙。请求对象本身是独立的,它不需要知道责任链的存在;责任链的组装发生在客户端或容器里,而不是在业务处理逻辑内部。
2.2 两种链式结构
实现责任链模式时,最常见的两种结构是链表式和集合式。
链表式:每个处理器内部持有下一个处理器的引用。客户端拿到链头之后,调用链头的handle方法,链头如果不用处理,就手动调用next.handle(request),请求就这样一个节点一个节点地传下去。经典 GoF 示例基本都是这种写法,它的优点是结构直观,缺点是线程安全、动态增删节点相对麻烦。
集合式:用一个List保存所有处理器,再用一个循环依次调用。执行到第i个处理器时,把“是否继续执行后续处理器”的控制权交给处理器本身。这种实现方式更贴近 Spring 等框架的工程实践,配合@Order注解可以方便地控制执行顺序,也更容易做动态插拔。
很多初学者认为链表式才是“正宗”责任链,这其实是个误解。从框架源码来看,集合式才是现代工程里更主流的写法。Servlet 的FilterChain、Spring MVC 的Interceptor,本质上都是对“处理器集合 + 顺序传递”的封装。
2.3 责任链与易混淆模式的边界
责任链模式经常被拿来和装饰器模式、策略模式、观察者模式比较,这里给出一个简单的区分表:
| 对比模式 | 核心关注点 | 与责任链的差异 |
|---|---|---|
| 装饰器模式 | 动态增强对象功能 | 装饰器链会完整执行所有包装层,且每个层都包裹原始对象;责任链则在某个节点处理后就可能停止 |
| 策略模式 | 在多个算法中选择一个 | 策略模式通常由客户端或上下文主动选择某个策略,责任链由链上的处理器自行决策是否接管 |
| 观察者模式 | 一对多通知 | 观察者模式中事件源广播事件,所有订阅者都能收到;责任链默认是单一请求被链上节点依次处理 |
一句话总结:策略模式是“选一个处理”,观察者模式是“通知所有人处理”,责任链模式是“按顺序问谁愿意处理,处理完就不问了”。这三者的区别,面试里被问到的概率很高,建议结合上面的表格理解,而不是死记定义。
3. 责任链模式一般用在什么场景
责任链模式在真实项目里的应用远比很多人想象中广泛,在 JDK、主流框架和中间件中都能看到它的身影。
第一个场景是审批流。请假、报销、采购这类业务,审批层级一般是固定的:组长只能批 3 天以内,部门经理能批 7 天以内,超过 7 天要总经理审批。如果用 if-else 写,每增加一个审批角色就要改一次核心逻辑。用责任链模式,每个审批角色都是一个独立处理器,按等级顺序组装成链,请求进来之后自动流转。这是责任链模式最经典的教材案例,也是我在实际项目中看到用得最多的场景。
第二个场景是过滤器与中间件。一个 HTTP 请求从进入到返回,往往要经过认证、鉴权、限流、参数校验、日志记录等多个环节。这些环节之间顺序固定,又每个都可能直接拒绝请求。Servlet 的Filter、Spring MVC 的HandlerInterceptor、Netty 的ChannelPipeline,都是责任链思想的落地实现。这也是我认为责任链模式“不需要刻意设计、它就在你身边”的最好例证。
第三个场景是日志框架。Logback、Log4j2 里的Logger配置了Level之后,日志事件会沿着 Logger 父子链向上传递,直到某个 Logger 设置了Additivity = false才停止。这本质上是责任链的变体:每个 Logger 处理完自己的日志事件后,决定是否继续传给父 Logger。
第四个场景是规则校验链。比如用户注册时要校验用户名、密码强度、手机号格式、邮箱格式;发布内容时要经过多道合规检查。这类场景非常适合把每一项校验做成独立处理器,放入一个链中,逐项执行。一旦某一项校验失败,就直接终止并返回错误信息,避免后续无效操作。
第五个场景是网关路由的前置后置处理器。在微服务架构中,网关对请求的预处理、对响应的后处理,也大量采用责任链思路。一个请求进来之后,可能依次经过签名校验、黑白名单校验、路由转发、响应包装多个节点。
看到这里你应该能理解,责任链模式适用的场景,都有三个显著特征:有多个处理节点、处理顺序相对固定、节点之间互不感知。反过来,如果处理节点很少、逻辑又高度耦合,强行引入责任链反而会让代码变得碎片化。所以责任链不是万能的,它是解决“处理链路持续扩展”问题的优雅方案。
4. 经典实现:Java 链表式审批链
4.1 场景假设
我们以请假审批为例。假设规则如下:
- 请假天数小于等于 2 天,由组长审批;
- 大于 2 天且小于等于 5 天,由部门经理审批;
- 大于 5 天,由总经理审批。
这是一个非常典型的责任链场景:请求是同一个“请假申请”,处理节点是三个不同级别的审批人,每个审批人只关心“是否在自己权限范围内”。
4.2 定义抽象处理者与请求实体
首先定义请假请求实体:
// 文件路径:com/example/chain/demo/LeaveRequest.java public class LeaveRequest { private String name; private int days; private String reason; public LeaveRequest(String name, int days, String reason) { this.name = name; this.days = days; this.reason = reason; } public String getName() { return name; } public int getDays() { return days; } public String getReason() { return reason; } }接着定义抽象处理者,重点是维护一个next引用,以及暴露给子类实现的handle方法:
// 文件路径:com/example/chain/demo/Approver.java public abstract class Approver { protected Approver next; public void setNext(Approver next) { this.next = next; } public abstract void handle(LeaveRequest request); }这里最关键的设计是:抽象处理者持有下一个处理者的引用,但它并不负责决定下一个是否执行,决策权完全交给子类。
4.3 实现具体处理者
实现三个具体审批人:
// 文件路径:com/example/chain/demo/TeamLeaderApprover.java public class TeamLeaderApprover extends Approver { @Override public void handle(LeaveRequest request) { if (request.getDays() <= 2) { System.out.println("组长审批通过:" + request.getName() + " 请假 " + request.getDays() + " 天,原因:" + request.getReason()); } else if (next != null) { next.handle(request); } } }// 文件路径:com/example/chain/demo/ManagerApprover.java public class ManagerApprover extends Approver { @Override public void handle(LeaveRequest request) { if (request.getDays() > 2 && request.getDays() <= 5) { System.out.println("部门经理审批通过:" + request.getName() + " 请假 " + request.getDays() + " 天,原因:" + request.getReason()); } else if (next != null) { next.handle(request); } } }// 文件路径:com/example/chain/demo/GeneralManagerApprover.java public class GeneralManagerApprover extends Approver { @Override public void handle(LeaveRequest request) { if (request.getDays() > 5) { System.out.println("总经理审批通过:" + request.getName() + " 请假 " + request.getDays() + " 天,原因:" + request.getReason()); } else if (next != null) { next.handle(request); } } }每个具体处理者的职责都很清晰:先判断自己能不能处理,不能处理就交给下一个。这里有个容易被忽略的细节:末尾节点如果没有兜底逻辑,且又不满足处理条件,请求就会在链的尽头被丢弃。真实项目中,建议在链尾增加一个兜底处理器,打印告警日志或抛出业务异常。
4.4 组装与运行
客户端负责组装链,并把请求交给链头:
// 文件路径:com/example/chain/demo/Client.java public class Client { public static void main(String[] args) { // 组装责任链 Approver teamLeader = new TeamLeaderApprover(); Approver manager = new ManagerApprover(); Approver generalManager = new GeneralManagerApprover(); teamLeader.setNext(manager); manager.setNext(generalManager); // 发起请求 LeaveRequest request1 = new LeaveRequest("张三", 1, "事假"); teamLeader.handle(request1); LeaveRequest request2 = new LeaveRequest("李四", 4, "年假"); teamLeader.handle(request2); LeaveRequest request3 = new LeaveRequest("王五", 10, "病假"); teamLeader.handle(request3); } }运行之后,输出结果如下:
组长审批通过:张三 请假 1 天,原因:事假 部门经理审批通过:李四 请假 4 天,原因:年假 总经理审批通过:王五 请假 10 天,原因:病假这个示例可以直接复制运行。你很快会发现链表式的局限:如果审批顺序变了,比如调整为“先经理、再组长”,就必须修改客户端中的setNext调用;如果要在链中间插入一个“HR 备案”节点,也需要调整整条链的组装代码。这在业务规则多变的系统里,维护成本会逐渐上升。所以接下来我们要看工程中更常用的集合式写法。
5. 工程推荐:基于集合与 Spring 注入的责任链
5.1 为什么工程中更常用集合式写法
链表式的setNext组装,在处理器数量少、顺序稳定的场景下非常直观。但在 Spring 项目中,我们通常希望处理器由容器统一管理,顺序通过注解声明,而不是在某个组装类里手动setNext。集合式责任链刚好能满足这种需求:把所有处理器注入到一个List里,再按@Order排序后依次执行。
这种写法的优势有三个:一是顺序调整成本低,改注解即可;二是新增处理器不影响原有代码,符合开闭原则;三是与 Spring 容器天然融合,团队其他人看到这种写法时可以很快理解。
5.2 定义过滤器接口与上下文
我们先定义一个通用的业务过滤器接口:
// 文件路径:com/example/chain/service/OrderFilter.java public interface OrderFilter { /** * 执行过滤处理 * * @param context 请求上下文 * @param chain 过滤器链 */ void doFilter(OrderContext context, OrderFilterChain chain); }再定义一个持有全部过滤器的链路类:
// 文件路径:com/example/chain/service/OrderFilterChain.java @Component public class OrderFilterChain { private final List<OrderFilter> filters; public OrderFilterChain(List<OrderFilter> filters) { // Spring 会自动把容器内所有 OrderFilter 实现类注入进来 this.filters = filters; } public void execute(OrderContext context) { for (OrderFilter filter : filters) { filter.doFilter(context, this); } } }这里有同学会问:为什么不直接在OrderFilterChain里调用filter.doFilter(context),而要再传一个chain?因为在真实场景里,某个过滤器可能希望“先处理一部分逻辑,然后继续走链,回来之后再处理剩余逻辑”,类似 Servlet Filter 的前置后置增强。把chain传进去,过滤器就拥有了控制权。不过在“简单顺序执行”的场景下,你也可以省略chain参数,直接遍历执行,代码会更简洁。
5.3 实现多个处理器
定义请求上下文对象,用来在多个过滤器之间传递数据:
// 文件路径:com/example/chain/service/OrderContext.java public class OrderContext { private String orderId; private String userId; private boolean blocked; // 省略 getter / setter }再实现两个具体过滤器,分别负责风控校验和库存扣减前置校验:
// 文件路径:com/example/chain/filter/RiskControlFilter.java @Component @Order(1) public class RiskControlFilter implements OrderFilter { @Override public void doFilter(OrderContext context, OrderFilterChain chain) { if ("black-user".equals(context.getUserId())) { context.setBlocked(true); System.out.println("风控过滤器:拦截黑名单用户 " + context.getUserId()); return; } System.out.println("风控过滤器:用户 " + context.getUserId() + " 通过风控校验"); } }// 文件路径:com/example/chain/filter/StockCheckFilter.java @Component @Order(2) public class StockCheckFilter implements OrderFilter { @Override public void doFilter(OrderContext context, OrderFilterChain chain) { if (context.isBlocked()) { return; } System.out.println("库存过滤器:订单 " + context.getOrderId() + " 库存校验通过"); } }5.4 用 Spring 容器统一管理
由于所有OrderFilter实现类都标注了@Component,Spring 在启动时会自动把它们注入到OrderFilterChain的构造函数里。@Order(1)、@Order(2)定义了过滤器之间的执行顺序。业务调用方只需要注入OrderFilterChain,调用execute方法:
// 文件路径:com/example/chain/demo/OrderService.java @Service public class OrderService { private final OrderFilterChain filterChain; public OrderService(OrderFilterChain filterChain) { this.filterChain = filterChain; } public void createOrder(OrderContext context) { filterChain.execute(context); if (context.isBlocked()) { System.out.println("下单被拦截,流程结束"); return; } System.out.println("订单创建成功:" + context.getOrderId()); } }这段代码有两点值得注意:第一,RiskControlFilter拦截后,StockCheckFilter通过context.isBlocked()判断中断执行,所以“是否继续传递”的控制权实际掌握在过滤器自己手里;第二,整个链路执行完之后,业务方法还需要根据上下文状态决定是否继续下单,这个“兜底决策”应该留在业务编排层,而不是某一级过滤器里。这是很多人写责任链容易搞混的地方:节点负责“拦截判断”,但整个链路的结果汇总由调用方负责。
6. 完整实战:内容合规处理链
6.1 场景说明
很多业务系统会对用户发布的内容做多道合规检查,比如格式校验、违禁词检测、广告信息识别、人工审核标记等。这类检查通常会按固定顺序执行,任意一道检查不通过,内容就会被标记为“不通过”,不再继续后续检查。
下面我用一个完整示例演示如何用责任链模式实现内容合规处理。这里的规则和示例都属于企业内部业务系统的通用合规管理流程,实际项目中请结合法律法规和平台规范定义自己的规则集。
6.2 核心代码
先定义统一的内容上下文:
// 文件路径:com/example/content/ContentContext.java public class ContentContext { private String content; private boolean passed = true; private String rejectReason; public ContentContext(String content) { this.content = content; } public String getContent() { return content; } public boolean isPassed() { return passed; } public void reject(String reason) { this.passed = false; this.rejectReason = reason; } public String getRejectReason() { return rejectReason; } }定义内容处理器的抽象接口:
// 文件路径:com/example/content/ContentProcessor.java public interface ContentProcessor { /** * 执行内容检查 * * @param context 内容上下文 * @return true 表示继续执行后续处理器,false 表示终止链路 */ boolean process(ContentContext context); }再定义链条执行器:
// 文件路径:com/example/content/ContentProcessorChain.java import java.util.List; public class ContentProcessorChain { private final List<ContentProcessor> processors; public ContentProcessorChain(List<ContentProcessor> processors) { this.processors = processors; } public boolean execute(ContentContext context) { for (ContentProcessor processor : processors) { boolean continueChain = processor.process(context); if (!continueChain || !context.isPassed()) { return false; } } return context.isPassed(); } }这里的终止策略是:只要有一个处理器判定内容不合格,或者某个处理器主动要求中断,链路就会停止。这样做既能避免无效计算,也能保证第一次拦截时就能拿到原因。
实现三个具体处理器:
// 文件路径:com/example/content/processor/LengthCheckProcessor.java public class LengthCheckProcessor implements ContentProcessor { @Override public boolean process(ContentContext context) { if (context.getContent() == null || context.getContent().length() < 2) { context.reject("内容长度不能少于 2 个字符"); return false; } System.out.println("长度检查通过"); return true; } }// 文件路径:com/example/content/processor/SensitiveWordProcessor.java public class SensitiveWordProcessor implements ContentProcessor { private final List<String> sensitiveWords; public SensitiveWordProcessor(List<String> sensitiveWords) { this.sensitiveWords = sensitiveWords; } @Override public boolean process(ContentContext context) { for (String word : sensitiveWords) { if (context.getContent().contains(word)) { context.reject("内容包含不被允许的词:" + word); return false; } } System.out.println("禁用词检查通过"); return true; } }// 文件路径:com/example/content/processor/AdvertCheckProcessor.java public class AdvertCheckProcessor implements ContentProcessor { @Override public boolean process(ContentContext context) { if (context.getContent().contains("加我微信") || context.getContent().contains("低价出售")) { context.reject("内容疑似包含广告营销信息"); return false; } System.out.println("广告信息检查通过"); return true; } }最后组装并运行:
// 文件路径:com/example/content/Main.java import java.util.Arrays; public class Main { public static void main(String[] args) { ContentProcessorChain chain = new ContentProcessorChain(Arrays.asList( new LengthCheckProcessor(), new SensitiveWordProcessor(Arrays.asList("违禁词A", "违禁词B")), new AdvertCheckProcessor() )); ContentContext context1 = new ContentContext("这是一段正常的用户评论内容"); boolean result1 = chain.execute(context1); System.out.println("内容1检查结果:" + (result1 ? "通过" : "不通过:" + context1.getRejectReason())); ContentContext context2 = new ContentContext("加我微信,低价出售商品"); boolean result2 = chain.execute(context2); System.out.println("内容2检查结果:" + (result2 ? "通过" : "不通过:" + context2.getRejectReason())); } }6.3 运行结果与验证
运行后预期输出如下:
长度检查通过 禁用词检查通过 广告信息检查通过 内容1检查结果:通过 长度检查通过 禁用词检查通过 广告信息检查不通过,原因:内容疑似包含广告营销信息 内容2检查结果:不通过:内容疑似包含广告营销信息观察输出可以发现,第二条内容虽然通过了长度检查和禁用词检查,但在广告信息检查处被拦截,后续没有继续执行任何处理器。这说明责任链的“短路”特性生效了。
如果内容在敏感词检查就失败,输出会变成这样:
长度检查通过 禁用词检查不通过,原因:内容包含不被允许的词:违禁词A 内容2检查结果:不通过:内容包含不被允许的词:违禁词A这种“失败即短路”的行为,在长链路场景下能明显减少无效计算。不同的业务对短路策略要求不同:有些场景要求“记录所有检查结果”,这时候就不应该短路,而应该让链走完;有些场景追求“首次失败立刻返回”,这时候短路就很重要。所以设计责任链接口时,一定要把“是否短路”作为明确的约定写进接口文档或代码注释里,否则后来的同事很容易改出与你预期相反的行为。
7. 责任链模式常见问题与排查思路
责任链模式概念简单,但工程落地时问题不少。以下是我整理的高频问题排查表:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 请求没有走到下一个处理器 | 当前处理器忘记调用next.handle()或未返回“继续”标记 | 在处理器入口和出口打印日志,观察链路走向 | 明确约定:不处理时必须调用next或返回true |
| 处理器执行顺序不对 | @Order写错,或没有排序逻辑 | 启动时打印注入的处理器列表及顺序 | 统一通过Ordered接口或@Order管理顺序,避免硬编码 |
| Spring 注入导致循环依赖 | 过滤器相互注入,或 FilterChain 与 Filter 互相引用 | 查看 Spring 启动日志中的循环依赖提示 | 依赖构造函数注入,避免双向依赖;用@Lazy缓解但不推荐 |
| 链上某个处理器抛异常,整个链路中断 | 处理器内没有捕获异常,异常处理策略不明确 | 检查异常堆栈,确认是哪个处理器抛出 | 定义统一的异常处理策略,记录异常并决定“终止链路”还是“跳过节点” |
| 某个请求被多个处理器重复处理 | 处理器没有做“是否归我管”的前置判断 | 打印每个处理器的入参和返回结果 | 每个处理器开头增加条件判断,明确处理边界 |
| 日志里看不到链路上下文 | 上下文对象没有传递traceId等信息 | 检查 Context 对象是否贯穿整个链路 | 统一使用 RequestContext 保存链路标识,方便串联日志 |
这里想重点强调第一个问题。链表式责任链里,经常出现“处理器 A 判断不归自己管,却忘了调用next.handle()”,结果请求莫名丢失,而且是偶发性的、只在某些参数下才出现。这种问题靠肉眼很难发现,最好的办法是在抽象处理者里做一次强制约束,比如把handle模板化:
public abstract class TemplateApprover extends Approver { @Override public void handle(LeaveRequest request) { boolean handled = doHandle(request); if (!handled && next != null) { next.handle(request); } } protected abstract boolean doHandle(LeaveRequest request); }这样每个子类只需要返回 “是否处理成功”,不需要惦记“要不要调用 next”,从机制上避免了忘调next的问题。这也是我强烈建议你在实际项目中采用的写法:把责任链的“传递机制”和“业务判断”拆开,不要让业务代码关心链是怎么传的。
8. 责任链模式最佳实践与工程建议
8.1 处理器尽量保持单一职责
每个责任链节点只做一件事。比如“校验登录态”和“校验权限”要拆成两个处理器,不要为了省事把它们揉在一起。单一职责不只是设计原则,更直接关系到链路复用:同样一套校验链,A 接口需要登录态校验和权限校验,B 接口可能只需要登录态校验。节点拆得越细,链的组装能力越强。
8.2 明确每个节点的三个决策
每个处理器在实现前,先回答三个问题:满足什么条件时由我来处理?处理完成之后是否继续传递?如果继续传递,我需要给后续节点传递什么数据?这三个问题有明确答案后,写出来的处理器边界会非常清晰。我见过的大部分“责任链改不动”的问题,根源都不是模式本身有问题,而是节点之间传参没有约定好。
8.3 用上下文对象而不是方法参数层层传递
责任链上节点多的时候,如果每个处理器都接收五六个参数,改动代价很大。建议定义一个统一的Context对象,把请求数据、处理状态、结果信息都放进去。这么做的好处是链路扩展时接口签名不用变,新增节点只改 Context 的字段即可。缺点是 Context 容易变成一个“上帝对象”,所以要注意按业务域拆分 Context,不要在一个 Context 里塞所有类型的数据。
8.4 链路必须要有日志和链路标识
生产环境排障时,最怕的就是链路长、但日志打得不全。建议在责任链入口生成一个traceId放进 Context,每个处理器打印入参、处理结果、耗时。配合日志采集系统,就能快速看到整个链路的执行情况:哪个节点耗时最长、哪个节点拦截了请求、哪个节点抛了异常。
8.5 终止条件与兜底处理
责任链的末尾一定要考虑“没人处理怎么办”。真实项目中,链尾通常放一个兜底处理器,记录告警日志,或者抛出带明确提示的业务异常。否则请求在链上流转一圈后悄无声息地结束,排查成本非常高。
8.6 尽量使用 Spring 的依赖注入管理处理器
如果你在 Spring 项目里用责任链,就不要手动new处理器了。把所有处理器作为 Spring Bean,注入到List中,配合@Order控制顺序。这样新增一个处理器只需要写一个类并注册为 Bean,不需要修改任何组装代码,最符合开闭原则。要特别注意,多个实现类注入到同一个List时,Spring 默认的顺序可能不符合预期,所以一定不要依赖“注入顺序就是声明顺序”,必须显式用@Order控制。
8.7 长链路要注意性能
责任链的每个节点都可能涉及 I/O、远程调用或复杂计算。如果链路很长且每次都完整执行,性能开销不可忽视。这时可以考虑三个优化方向:一是支持“短路”,尽早拦截;二是支持条件化跳过,比如某些节点在特定开关下不执行;三是异步化,将非核心节点的执行移到独立线程池中,但要评估异步后的结果一致性问题。
8.8 不要为模式而模式
最后一条,也最重要。如果现在的代码只有两三个固定分支,且未来几乎不会扩展,不要为了“用设计模式”而硬上责任链。设计模式是为了降低变化带来的成本,而不是为了让代码看起来很高级。判断标准很简单:如果未来“新增一个处理节点”会让你改动现有的核心逻辑,那才值得用责任链;如果新增节点必然导致一堆调用方代码改动,那责任链也是治标不治本。
9. 什么时候不要用责任链模式
前面说了很多责任链的适用场景,这里必须把边界讲清楚。
第一种情况,处理节点少且顺序永远不会变。比如某个方法里只有两个固定的前置校验,优先级、流程都写死在需求里,未来也没有扩展计划。用责任链会把这个简单逻辑拆成五六个类,反而增加了阅读成本。
第二种情况,节点之间有强数据依赖。假如第二个处理器的计算强依赖第一个处理器的中间结果,而且这个依赖不是“上下文传递”就能解耦的,那么多个处理器之间会隐式耦合。这种情况下,硬拆责任链只会让隐式依赖变得更难发现。
第三种情况,你真正需要的是“遍历所有节点”,而不是“第一个能处理的人”。比如灰度发布时要给所有观察者推送事件,这是观察者模式而不是责任链。责任链的默认语义是“请求只会被链上的某些节点处理,处理完可能就终止”,如果你的需求是“所有节点都必须处理”,那应该选别的模式。
第四种情况,团队对设计模式不熟悉,且当前代码没有明显痛点。在团队协作中,代码的可读性优先于设计感。如果团队里大多数人第一次接触责任链,而当前代码按顺序写 if 已经足够直观,那你应该优先考虑团队整体的可维护性,而不是个人对设计模式的追求。
10. 总结与下一步实践建议
责任链模式解决的核心问题,是把“请求发送者”和“请求处理者”彻底解耦,让多个处理者按顺序组成链路,由每个处理者自己决定是否接管。它最典型的应用是审批流、过滤器链、规则校验链,在 Servlet、Spring、Netty、Logback 等框架中都有成熟实现。工程落地时,我更推荐集合式责任链配合 Spring 容器管理,用@Order控制顺序,用统一 Context 传递数据,用 traceId 串联日志。
如果你想进一步实践,我建议按下面三步走:
- 去读 Servlet 的
FilterChain源码和 Spring MVC 的HandlerInterceptor源码,重点看它们是如何维护处理器列表、如何控制“是否继续执行”的。这两个源码是你理解责任链工程落地的最佳教材。 - 从自己的项目里找一个“入口方法中按顺序堆了很多 if”的案例,先把处理链路画出来,确认节点顺序和终止条件,再试着用责任链重写一遍。
- 写完实现后,增加一个全新的处理节点,看看你的设计是否需要改动原有代码。如果完全不用改,恭喜你,责任链模式已经真正落地了。
设计模式的学习有个规律:概念背得再熟,不如在真实代码里重构一次。责任链模式尤其如此,它看起来只是链表和循环的组合,但真正值钱的,是你对链路节点职责边界、终止策略、顺序管理、失败兜底这些细节的把握。把这一层想明白,你在系统设计里就比别人多了一个“应对处理链路持续变化”的武器。