
这次我们直接用代码把责任链模式讲清楚它是什么、为什么出现、一般用在哪些场景以及怎么在 Java 和 Spring Boot 项目里落地。如果你正在复习 Java 设计模式、准备设计模式期末考试或者写设计模式大作业时被“多级审批”“请求过滤”这类需求卡住这篇文章可以直接收藏。责任链模式不是那种看起来高大上、实际用不上的概念它在 Servlet Filter、Spring MVC 拦截器、Netty ChannelPipeline 里都是核心骨架。把它的原理吃透你既能看懂框架源码也能在自己项目里把一堆 if-else 改成可扩展的链路。文章不会只讲 UML 类图重点放在三件事第一责任链模式到底解决了什么代码痛点第二用 Java 手写一套可运行的审批链和过滤器链第三把它接入 Spring Boot看怎么利用自动装配把多个处理器串起来。最后会给出测试用例、常见坑和最佳实践读完你可以直接照着写。1. 责任链模式核心能力速览项目说明模式类型行为型设计模式核心思想多个处理者组成一条链请求从链头进入依次传递直到某个处理者处理它主要解决问题请求发送者与接收者强耦合、多条件分支膨胀、处理流程难以动态调整核心角色Handler处理者、ConcreteHandler具体处理者、Chain链路、Client请求发起者实现语言Java文中提供可直接运行的完整代码是否需要框架不需要JDK 原生即可实现典型框架案例Servlet Filter、Spring MVC HandlerInterceptor、Netty ChannelPipeline、MyBatis Plugin是否支持动态增删节点支持增加一个处理者不影响其他节点是否支持顺序控制支持节点的组装顺序决定处理顺序适合读者Java 开发、学生、准备面试或希望优化业务代码结构的开发者责任链最值钱的一点是“开闭原则”新增一个处理规则不需要改动请求发送方也不需要改动其他处理者只需要把新节点挂到链上。这个特点在业务规则经常变化的场景里非常实用。2. 责任链模式要解决什么问题2.1 从一段难维护的代码说起假设现在要做一个请假审批功能。规则是这样的请假 3 天以内组长审批7 天以内经理审批超过 7 天总监审批。第一版代码通常长这样public class ApprovalService { public void approve(LeaveRequest request) { if (request.getDays() 3) { System.out.println(组长审批); } else if (request.getDays() 7) { System.out.println(经理审批); } else { System.out.println(总监审批); } } }这段代码的问题在规则少的时候不明显一旦出现下面这些变化就很难受了新增一个“部门负责人”角色审批逻辑又要加一个 else if。审批规则从“按天数”变成“按金额”“按职级”if-else 会越来越长。某个请求需要先走敏感词校验、再走审批、最后发通知发送方必须知道完整流程。测试时想单独验证“组长”这一层必须把整个方法跑一遍。本质问题是请求的发送方和所有处理逻辑耦合在同一个方法里。发送方并不关心具体是谁在处理它只关心请求最终被处理了。2.2 用责任链重构后的效果责任链模式的解法是把每个审批角色拆成独立的处理者然后串成链。请求从链头进入每个处理者判断“能不能处理”能处理就处理不能处理就传给下一个。LeaveHandler teamLeader new TeamLeaderHandler(); LeaveHandler manager new ManagerHandler(); LeaveHandler director new DirectorHandler(); teamLeader.setNext(manager); manager.setNext(director); teamLeader.handle(new LeaveRequest(张三, 2));重构后的特点很明显发送方只认识链头teamLeader不关心后面是谁。新增审批角色只需写一个新的 Handler挂到链上。处理顺序由setNext的组装顺序决定调整顺序不需要修改 Handler 内部代码。每个 Handler 可以单独测试。2.3 责任链模式的核心概念责任链模式的官方定义是避免请求发送者与接收者耦合让多个对象都有机会处理请求将这些对象连成一条链并沿着链传递请求直到有一个对象处理它。这里面有几个关键点发送者不持有接收者的具体引用只持有链头。每个处理者都要决定两件事自己能否处理以及处理不了时往哪传。请求可以在任意一个节点被处理并终止也可以被所有节点依次处理后再结束。这两类分别对应“推模型”和“拉模型”。推模型是 Handler 主动调用下一个 Handler前面审批链就是推模型。拉模型是链容器内部循环查找能处理的 Handler。日常开发中推模型更常见后面的代码也以推模型为主。3. 责任链模式适用场景与边界3.1 典型适用场景责任链模式的应用场景很固定判断标准只有一个同一个请求存在多个候选处理者但最终只需要其中某一个或某几个处理它并且处理顺序有讲究。常见的落地场景审批流请假审批、报销审批、合同审批不同金额、天数、职级走不同角色。请求过滤登录校验、权限校验、限流、参数校验全部通过才放行。内容安全敏感词过滤、广告检测、违禁内容拦截命中即短路。日志分级Debug、Info、Error 各自处理对应级别的日志。异常处理不同类型的异常交给不同的异常处理器。订单状态流转创建订单、支付校验、库存扣减、发货通知按固定顺序执行。框架层面的 Filter 和 InterceptorServlet Filter、Spring MVC 拦截器、Netty Pipeline 都是责任链思想的工程化实现。以 Spring MVC 的拦截器为例一个请求会依次经过 preHandle只要其中一个返回 false链路就中断。这里面的 InterceptorExecutionChain 本质就是一条责任链。3.2 不适合用责任链的场景责任链不是万能的下面几种情况不建议硬套处理者只有一个不需要链直接用策略模式或普通方法调用更简单。请求必须一次性拿到所有处理结果做汇总责任链的“短路”特性反而碍事。链路过长每个节点都有远程调用性能会叠加需要评估是否合并节点或改成异步管道。处理顺序经常变化且节点之间有强依赖责任链的顺序控制会变成维护负担。一句话总结链的长度和节点数量失控时责任链的可读性会下降。不要让一条链超过七八个节点否则排查问题成本很高。3.3 使用边界与合规提示责任链模式只解决代码结构问题不解决业务安全问题。如果使用责任链做内容过滤、敏感词处理、鉴权等能力需要注意敏感词库、拦截规则必须按照平台规则和相关法律法规配置不能随意收集或使用用户隐私数据。日志中不要打印完整请求内容尤其是涉及手机号、身份证、账号等敏感信息需要脱敏。批量处理用户内容时要确认是否有合法授权处理结果需要人工复核不能完全依赖自动化过滤。责任链中的每个 Handler 都要有明确的职责边界不要在链里偷偷做数据收集等无关操作。4. 环境准备与前置条件文中代码基于 Java 8 编写Spring Boot 示例使用 Spring Boot 2.x 之上的自动化装配能力。实际操作时按下面的清单准备即可项目要求JDKJDK 8 及以上IDEIDEA 或 Eclipse 均可构建工具Maven 或 Gradle不必须直接用 javac 也行Spring 示例Spring Boot 2.x 或 3.x测试框架JUnit 4 或 JUnit 5用于写验证用例不依赖任何第三方组件。责任链模式的核心逻辑用 JDK 原生语法就能写完这也是它适合作为设计模式入门的原因。5. Java 手写责任链完整代码落地下面从零写一套请假审批责任链。代码分四部分请求对象、抽象处理者、具体处理者、客户端组装。5.1 定义请求对象public class LeaveRequest { private final String applicant; private final int days; public LeaveRequest(String applicant, int days) { this.applicant applicant; this.days days; } public String getApplicant() { return applicant; } public int getDays() { return days; } Override public String toString() { return LeaveRequest{applicant applicant , days days }; } }请求对象只负责携带数据不包含任何审批逻辑。这是责任链的重要约定数据和处理逻辑分离请求本身不知道谁会处理自己。5.2 抽象处理者用模板方法固定链的流转抽象处理者是责任链的核心它定义了两件事如何拼装下一级节点setNext。如何判断处理还是传递handle。把判断逻辑做成抽象方法canApprove把具体处理动作做成抽象方法doApprovehandle方法使用模板方法模式固定整个流转过程public abstract class LeaveHandler { protected LeaveHandler next; public LeaveHandler setNext(LeaveHandler next) { this.next next; return this; } public void handle(LeaveRequest request) { if (canApprove(request)) { doApprove(request); } else if (next ! null) { next.handle(request); } else { System.out.println(当前链路无人能审批 request.getDays() 天); } } protected abstract boolean canApprove(LeaveRequest request); protected abstract void doApprove(LeaveRequest request); }这样写的好处是具体处理者只需要关心“自己能不能处理”和“处理时做什么”不需要关心链路如何流转。即使某个节点忘记调用 next也不会导致整个流程不可控因为链的流转逻辑已经被抽象类固定住了。5.3 具体处理者public class TeamLeaderHandler extends LeaveHandler { Override protected boolean canApprove(LeaveRequest request) { return request.getDays() 3; } Override protected void doApprove(LeaveRequest request) { System.out.println(组长已审批 request.getApplicant() 请假 request.getDays() 天); } }public class ManagerHandler extends LeaveHandler { Override protected boolean canApprove(LeaveRequest request) { return request.getDays() 7; } Override protected void doApprove(LeaveRequest request) { System.out.println(经理已审批 request.getApplicant() 请假 request.getDays() 天); } }public class DirectorHandler extends LeaveHandler { Override protected boolean canApprove(LeaveRequest request) { return true; } Override protected void doApprove(LeaveRequest request) { System.out.println(总监已审批 request.getApplicant() 请假 request.getDays() 天); } }总监节点的canApprove直接返回 true相当于链尾兜底。这样超过所有层级处理范围的请求也一定会有最终结果不会出现请求进入链后静默消失的情况。5.4 组装链路与发送请求public class LeaveChainClient { public static void main(String[] args) { LeaveHandler teamLeader new TeamLeaderHandler(); LeaveHandler manager new ManagerHandler(); LeaveHandler director new DirectorHandler(); teamLeader.setNext(manager); manager.setNext(director); LeaveRequest r1 new LeaveRequest(张三, 2); LeaveRequest r2 new LeaveRequest(李四, 5); LeaveRequest r3 new LeaveRequest(王五, 15); teamLeader.handle(r1); teamLeader.handle(r2); teamLeader.handle(r3); } }输出结果组长已审批张三 请假 2 天 经理已审批李四 请假 5 天 总监已审批王五 请假 15 天这段代码说明责任链最典型的两种能力链越长、规则越多客户端代码不需要变化请求在哪个节点被处理完全由各节点的canApprove决定。发送方永远只调用链头的handle。5.5 一套更贴近框架源码的 Filter Chain 写法上面的审批链属于“处理即终止”的责任链。但很多场景要求每个节点都有机会处理请求比如登录、权限、日志每个过滤器都要执行并且可以决定是否继续往下走。这种写法更接近 Servlet Filter 和 Spring MVC 拦截器的实现。public interface Filter { void doFilter(Request request, Response response, FilterChain chain); }public class FilterChain { private final ListFilter filters new ArrayList(); private int index 0; public FilterChain addFilter(Filter filter) { filters.add(filter); return this; } public void doFilter(Request request, Response response) { if (index filters.size()) { return; } Filter filter filters.get(index); filter.doFilter(request, response, this); } }public class LogFilter implements Filter { Override public void doFilter(Request request, Response response, FilterChain chain) { System.out.println(LogFilter 前置逻辑); chain.doFilter(request, response); System.out.println(LogFilter 后置逻辑); } }public class AuthFilter implements Filter { Override public void doFilter(Request request, Response response, FilterChain chain) { if (request.getToken() null) { System.out.println(AuthFilter 拦截未登录); return; } System.out.println(AuthFilter 校验通过); chain.doFilter(request, response); } }这里的关键是真正的链对象被当作参数传给了每个 Filter。Filter 想拦截就 return想放行就调用chain.doFilter。框架层之所以能在一个请求里执行多段“前置后置逻辑”靠的就是这种写法。6. 功能测试与效果验证责任链的效果验证重点是三条请求是否走到了正确的节点、链尾是否兜底、顺序是否可控。用 JUnit 写几个用例public class LeaveChainTest { Test void shouldApproveByTeamLeaderWhenDaysLessThanOrEqualTo3() { LeaveHandler teamLeader new TeamLeaderHandler(); teamLeader.setNext(new ManagerHandler()); teamLeader.handle(new LeaveRequest(张三, 3)); // 期望输出组长已审批张三 请假 3 天 } Test void shouldApproveByManagerWhenDaysBetween4And7() { LeaveHandler handler buildChain(); handler.handle(new LeaveRequest(李四, 5)); // 期望输出经理已审批李四 请假 5 天 } Test void shouldRejectWhenNoOneCanHandle() { LeaveHandler handler new TeamLeaderHandler(); handler.handle(new LeaveRequest(赵六, 100)); // 期望输出当前链路无人能审批100 天 } private LeaveHandler buildChain() { LeaveHandler teamLeader new TeamLeaderHandler(); LeaveHandler manager new ManagerHandler(); LeaveHandler director new DirectorHandler(); teamLeader.setNext(manager); manager.setNext(director); return teamLeader; } }测试时建议做三个额外验证打日志看链路节点顺序确认新增节点后老节点逻辑没被改坏。把链头换成中间节点比如直接从 manager 开始调用观察请求是否仍能继续向后走验证链路不依赖入口。故意让一个节点不调next.handle观察请求是否会停止确认每个节点的处理逻辑是否完整。如果某次请求没有走到预期节点优先检查两处该节点canApprove的判断条件是否覆盖了当前参数前一个节点是否在分支结构里漏掉了对next的调用。7. Spring Boot 整合责任链自动化装配与排序真实项目中很少手动setNext更多的是把 Handler 交给 Spring 管理利用自动装配把同一接口的所有实现收集起来再按顺序执行。下面做一个内容发布前的链路敏感词拦截、长度校验、发布成功。三个节点全部通过才发布任一节点命中就中止。7.1 定义处理接口与门面对象public interface TextCheckHandler { int order(); void check(ContentRequest request, CheckChain chain); }public class ContentRequest { private final String text; private boolean blocked; private String blockReason; public ContentRequest(String text) { this.text text; } public String getText() { return text; } public boolean isBlocked() { return blocked; } public void markBlocked(String reason) { this.blocked true; this.blockReason reason; } public String getBlockReason() { return blockReason; } }public class CheckChain { private final ListTextCheckHandler handlers; private final int index; public CheckChain(ListTextCheckHandler handlers, int index) { this.handlers handlers; this.index index; } public void doNext(ContentRequest request) { if (index handlers.size()) { return; } TextCheckHandler handler handlers.get(index); CheckChain nextChain new CheckChain(handlers, index 1); handler.check(request, nextChain); } }7.2 多个 Handler 通过 Spring 自动注入Component public class SensitiveWordHandler implements TextCheckHandler { Override public int order() { return 10; } Override public void check(ContentRequest request, CheckChain chain) { if (request.isBlocked()) { return; } if (request.getText().contains(加微信)) { request.markBlocked(命中营销敏感词); return; } chain.doNext(request); } }Component public class LengthCheckHandler implements TextCheckHandler { Override public int order() { return 20; } Override public void check(ContentRequest request, CheckChain chain) { if (request.isBlocked()) { return; } if (request.getText().length() 200) { request.markBlocked(文本超长); return; } chain.doNext(request); } }Configuration public class CheckChainConfig { Bean public CheckChain checkChain(ListTextCheckHandler handlers) { ListTextCheckHandler sorted handlers.stream() .sorted(Comparator.comparingInt(TextCheckHandler::order)) .collect(Collectors.toList()); return new CheckChain(sorted, 0); } }Service public class ContentPublishService { private final CheckChain checkChain; public ContentPublishService(CheckChain checkChain) { this.checkChain checkChain; } public void publish(String text) { ContentRequest request new ContentRequest(text); checkChain.doNext(request); if (request.isBlocked()) { System.out.println(发布被拦截 request.getBlockReason()); } else { System.out.println(发布成功); } } }Spring 启动时会自动扫描所有TextCheckHandler的实现类注入到checkChain方法中然后按order排序组装成链。以后新增一个规则只需要写一个新的 Handler 并标注Component不需要修改已有的 Service 和配置类。这是责任链模式在 Spring 项目中最常用的整合方式。7.3 效果验证方式验证时直接调用ContentPublishServiceSpringBootTest public class ContentPublishServiceTest { Autowired private ContentPublishService publishService; Test void blockedWhenContainsSensitiveWord() { publishService.publish(加微信领取优惠券); // 期望输出发布被拦截命中营销敏感词 } Test void blockedWhenTextTooLong() { StringBuilder sb new StringBuilder(); for (int i 0; i 210; i) { sb.append(测); } publishService.publish(sb.toString()); // 期望输出发布被拦截文本超长 } Test void publishSuccessWhenAllPassed() { publishService.publish(今天天气不错); // 期望输出发布成功 } }如果输出结果不对先看order是否排序正确再看每个 Handler 的check方法是否在命中后正确 return、未命中时是否调用chain.doNext。这种链路最常见的问题就是把doNext放在了错误的分支里。8. 责任链模式与相邻设计模式对比责任链模式和策略模式、装饰器模式、模板方法模式在结构上有点像面试时经常被放在一起问。它们的定位差异必须分清。对比项责任链模式策略模式模板方法模式装饰器模式观察者模式核心意图多个处理者依次尝试处理同一个请求运行时选择一种算法固定步骤骨架子类实现细节动态增强对象功能状态变化通知多个观察者关键区别请求沿链传递直到被处理一次只选一个策略执行流程固定实现可变功能按层叠加广播通知不关心谁处理典型例子Servlet Filter、审批流支付方式选择JdbcTemplateIO 流包装事件监听最容易混淆的是责任链和装饰器模式装饰器模式是多个对象“嵌套包装”调用时像剥洋葱一样一层一层执行每一层都会执行而且通常不中断。责任链模式是多个对象“平级串联”请求走到某个节点时可能被处理并终止。责任链和策略模式的区别更直接策略模式是“一堆算法里选一个”责任链是“一堆处理者依次试谁合适谁处理”。策略模式没有链式传递的概念责任链适合处理“谁有资格处理”的判定过程。9. 责任链模式常见问题与排查方法问题现象可能原因排查方式解决方案请求进入链后没有任何输出链头对象没有组装下级节点检查 setNext 是否被调用确认链尾有兜底 Handler链路处理顺序不对添加 Handler 的顺序或 order 排序错误打印每个节点的类名和 order统一用 order 排序不要在代码里手动散落组装某个节点执行后链路中断该节点处理完没有调用 next/chain.doNext查看该 Handler 的分支逻辑确认所有不拦截的分支都调用了 doNext链路出现循环调用next 指针指回自己检查 setNext 调用使用 List 一次性组装避免手工设置 next并行请求数据互相污染Handler 内部使用了有状态成员变量检查是否有共享字段Handler 设计为无状态数据全部放到请求对象里Spring 注入的 List 缺少某些 Handler这些 Handler 没加 Component查看包扫描路径检查 ComponentScan 扫描范围全链路性能下降明显每个 Handler 都做数据库查询或远程调用打印每个节点执行耗时合并节点、加缓存或改为异步执行排查时有一个通用技巧在抽象类的handle方法或CheckChain.doNext中加一条日志打印当前节点的类名。这样请求走到哪一步一目了然能快速定位断点。10. 责任链模式最佳实践与工程建议10.1 控制链路长度一条责任链最好控制在 5 到 7 个节点以内。节点太多时可以按业务维度拆成多条独立链而不是把所有规则塞进一条链。比如内容发布场景可以拆成“内容安全链”和“格式校验链”链之间用编排层串联。10.2 Handler 必须无状态多个请求会共用同一个 Handler 实例因此 Handler 内部不要保存与当前请求相关的数据。所有中间结果都放到请求对象里传递例如上面的ContentRequest。如果确实需要缓存用 ThreadLocal 要谨慎请求结束后必须清理。10.3 链路组装集中管理不要在多个业务方法里分别手工setNext。应该在配置类或工厂方法中统一组装例如 Spring Boot 里的CheckChainConfig。这样链路结构集中、可读性强排查问题时一眼能看到整条链有哪些节点。10.4 日志与可观测性线上排查责任链问题最怕没有日志。建议每个 Handler 打印统一的日志格式[链路] 节点敏感词校验, 结果通过, 耗时2ms敏感信息要脱敏不要打印完整请求内容。批量任务场景下可以给请求带上 requestId日志里带上这个 ID整条链路的调用轨迹就能串起来。10.5 批量与并发场景责任链本身适合批量任务一批请求共用同一条链顺序遍历每个请求即可。但要注意如果一个 Handler 内部有状态批量并发时会互相覆盖。工程化做法是把 Handler 做成无状态单例请求数据全部封装在请求对象中批量任务使用线程池时每个线程只处理自己的请求对象不共享状态。10.6 合规与安全用责任链做内容安全、鉴权、过滤时要遵守业务所在平台的规则和法律法规。敏感词库要由合法来源配置拦截逻辑要保留审计日志但对用户内容要脱敏处理。涉及自动拦截的批量任务建议在正式发布前做一轮人工复核避免误伤正常内容。11. 总结与下一步责任链模式最值得先跑通的是第 5 节那套审批链代码把if-else改造成链式调用后你会直观感受到新增一个处理规则有多轻量只需要写新 Handler、挂到链上旧代码一行都不用动。这也是它作为高频设计模式被反复考察的原因。最容易踩的坑有三个组装链路时忘了设置兜底节点、某个 Handler 在分支里漏掉了chain.doNext、Spring 场景下 Handler 没有注册成 Bean。这三个问题都能通过加日志快速定位。下一步可以往两个方向深入一是阅读 Netty ChannelPipeline 和 Spring Security FilterChainProxy 的源码看工业级框架怎么组织长链路并实现动态增删节点二是把责任链模式与管道模式对比理解在“必须全链执行”的场景下管道模式为什么比责任链更合适。第一步可以先拿自己项目里一段最长的 if-else 试试把每个分支改成一个 Handler。改完之后你会发现设计模式的收益不是代码量变少了而是以后每次改需求都不需要再翻那坨大分支了。