大家好,我是你们的技术博主。今天我们聊一个在业务开发中出镜率奇高、但许多初学者一看到类名就发懵的设计模式——责任链模式。
之前在项目里接到一个需求:不同金额的采购单要走不同的审批流程;请假超过 3 天要总经理批,3 天以内部门经理就能定。第一版代码我写了一大串if ... else if ...,每次新增一个审批角色,就要改动核心方法,改到后来自己都分不清哪条分支生效。后来重构时换成责任链模式,不仅代码清爽了,后续加审批节点也只需要新增一个类,不再动老代码。
这篇文章就从实际场景切入,把责任链模式的原理、代码实现、典型应用、框架中的踪迹、常见坑点,一次性讲透。文章内容比较多,建议收藏后静下心来看。
1. 责任链模式是什么?解决什么问题?
1.1 从现实问题说起:if else 的噩梦
先来看一个非常常见的业务场景:请假审批。
假设现在公司规定是这样的:
- 请假天数小于等于 2 天,由组长审批;
- 请假天数大于 2 天且小于等于 5 天,由部门经理审批;
- 请假天数大于 5 天,由总经理审批。
很多人的第一反应是写这样一个方法:
public class LeaveService { public String approve(LeaveRequest request) { int days = request.getDays(); if (days <= 2) { return "组长审批通过"; } else if (days <= 5) { return "部门经理审批通过"; } else { return "总经理审批通过"; } } }这段代码在需求固定的情况下没有任何问题。但业务是不断变化的,很快产品经理又提了新需求:
- 请假 10 天以上需要 CEO 审批;
- 每天请假人数超过 50 人时,需要 HR 部门备案。
于是你的if else越来越多,方法越来越长。更糟糕的是,如果审批顺序发生变化,比如“先由 HR 备案,再由组长审批”,你不得不继续改写这个方法的内部结构。
这种写法的核心问题在于:请求的接收者和处理者被硬编码在了一起,每新增一种处理逻辑,都要修改已有代码,违反了开闭原则(对扩展开放,对修改关闭)。
1.2 责任链模式的定义
责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式。它的核心思想是:
将能够处理同一类请求的多个对象连成一条链,提交一个请求沿链传递,链上的每个对象依次判断自己能否处理这个请求。如果能处理,就处理;如果不能处理,就把请求转发给下一个对象。
换一句更通俗的话:避免请求发送者与多个处理者之间产生强耦合,让多个对象都有机会处理请求,并且由链式结构动态决定请求最终由谁处理。
这里有一个关键点:发送者并不知道也不关心最终是哪个对象处理了请求,它只需要把请求丢到链上即可。处理顺序、处理策略完全由链条的构建方式决定。
1.3 责任链模式的四个核心角色
责任链模式通常包含 4 个角色:
| 角色 | 职责 |
|---|---|
| 抽象处理者(Handler) | 定义处理请求的接口,并且持有下一个处理者的引用 |
| 具体处理者(ConcreteHandler) | 实现抽象处理者,判断自己能否处理请求,不能处理则交给下一个 |
| 请求对象(Request) | 封装请求数据 |
| 客户端(Client) | 创建责任链,并向链头发送请求 |
抽象处理者的核心定义如下:
public abstract class Approver { protected Approver next; public void setNext(Approver next) { this.next = next; } // 由子类实现具体的审批逻辑 public abstract void handle(LeaveRequest request); }每个具体处理者都持有下一个处理者的引用,形成一条单向链表。这也就是“责任链”名字的由来。
2. 责任链模式的两种实现方式
责任链模式在代码实现上,可以分成两种常见形式:链表式责任链和数组式责任链。它们各有适用场景。
2.1 链表式责任链
链表式是责任链模式的经典实现,也是 GoF 设计模式书中描述的形态。
每个处理者内部持有下一个处理者的引用,通过setNext()方法串联。当处理完当前节点后,如果需要传递,就调用next.handle(request)。
它的优点是:
- 结构直观,代码好理解;
- 链的长度可以动态调整;
- 可以在链中间插入或删除节点。
缺点是:
- 如果链很长,调用栈会比较深;
- 每个节点都要维护 next 引用,职责上有一点冗余。
链表式责任链适用于处理节点数量相对固定、调用链比较清晰的场景,比如请假审批、报销审批。
2.2 数组式责任链
数组式责任链,也叫“列表式责任链”,是把所有处理者放在一个List中,通过索引遍历。
List<Handler> handlers = new ArrayList<>(); handlers.add(new GroupLeaderHandler()); handlers.add(new ManagerHandler()); handlers.add(new BossHandler()); for (Handler handler : handlers) { if (handler.canHandle(request)) { handler.handle(request); break; } }这种方式在业务系统中使用得也非常广泛,尤其是在中间件、拦截器、过滤器的设计中。
它的优点是:
- 链路配置非常灵活,可以利用 Spring 的依赖注入自动收集所有处理器;
- 便于实现“某个处理器不满足条件就跳过”的便捷逻辑;
- 调用栈浅,调试方便。
缺点是:
- 每个处理者仍然需要具备
canHandle判断能力,否则语义不清; - 如果删除某个节点,需要操作 List 而不是节点本身。
在实际工程中,两种方式可以结合。比如用数组方式收集和排序所有处理器,内部处理时再通过“链式调用”把请求传递下去。
3. 责任链的传递与控制:三种请求处理策略
很多初学者只知道责任链是“一条链顺着往下传”,但不知道其实责任链有三种常见的处理策略。理解了这三种策略,你才能在不同的业务需求中选择正确的实现方式。
3.1 策略一:全链路遍历,每个节点都处理
这种策略下,请求从链头开始,每个处理者都会执行自己的逻辑,直到链结束。适用于“日志记录”、“参数校验”、“敏感词过滤”等场景。
比如日志框架中,一条日志可能要经过格式化、过滤、输出等多个环节,每个环节都需要参与:
public class LogInterceptorChain { private List<Interceptor> interceptors; private int index = 0; public void proceed(LogRequest request) { if (index >= interceptors.size()) { return; } Interceptor interceptor = interceptors.get(index++); // Do something interceptor.intercept(request, this); } }每个拦截器处理完自己的事情之后,再调用chain.proceed()把请求传给下一个。这就是“全链路都要处理”的典型实现。
3.2 策略二:找到第一个能处理的节点,立即停止
这种策略对应我们之前说的审批场景。请求从链头开始,遇到第一个具备处理能力的节点,就由该节点处理,后面的节点不再执行。
public abstract class Handler { protected Handler next; public void handle(Request request) { if (canHandle(request)) { doHandle(request); } else if (next != null) { next.handle(request); } else { // 无人处理 throw new UnsupportedOperationException("没有处理器"); } } protected abstract boolean canHandle(Request request); protected abstract void doHandle(Request request); }这种策略的核心是canHandle()判断逻辑。每个节点自己决定“我能不能处理这个请求”,不能则向下传递。优点是请求在遇到正确节点后立即返回,执行效率高;缺点是如果链条构建顺序有误,请求可能被错误节点提前拦截。
3.3 策略三:可中断的责任链
这是“全链路遍历”的进阶版。每个节点都处理请求,但任何一个节点都可以主动中断传递。这种策略广泛应用于网关、拦截器、认证授权等场景。
比如 Spring MVC 的拦截器HandlerInterceptor,preHandle返回false就中断了后续拦截器和 Controller 的执行:
public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("token"); if (token == null || token.isEmpty()) { response.setStatus(401); return false; // 中断链路 } return true; } }这里返回的boolean就决定了链路是否继续向下传递。这种方式比“逐层向下传递”更加灵活,也更符合 Web 应用的现实需求。
4. 完整实战案例:请假审批系统
理论讲完了,接下来我们通过一个完整的请假审批系统案例,把责任链模式从头到尾写一遍。为了让代码更贴近真实工程,我会使用 Spring Boot 的组件扫描思想来装配责任链,同时也会给出手动构建链的版本。
4.1 项目结构与依赖
我们的项目是一个普通的 Maven Java 项目,不需要额外的第三方框架。JDK 版本建议 8 以上。
chain-of-responsibility-demo/ ├── pom.xml └── src/main/java/com/example/demo/ ├── LeaveRequest.java ├── Approver.java ├── GroupLeaderApprover.java ├── ManagerApprover.java ├── BossApprover.java └── Client.javapom.xml只需要基本配置即可:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>chain-of-responsibility-demo</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> </project>4.2 定义请求对象
首先定义请假请求对象,封装请假人的信息和请假天数。
package com.example.demo; /** * 请假请求 */ public class LeaveRequest { private String name; private int days; public LeaveRequest(String name, int days) { this.name = name; this.days = days; } public String getName() { return name; } public int getDays() { return days; } }这里只保留了两个核心字段:申请人姓名name和请假天数days。你完全可以按业务需要扩展,比如请假类型、开始时间、结束时间、原因等。
4.3 定义抽象处理者
抽象处理者规定了责任链的基本骨架。
package com.example.demo; /** * 抽象审批人 */ public abstract class Approver { protected Approver next; /** * 设置下一个审批人 */ public void setNext(Approver next) { this.next = next; } /** * 审批方法:由子类实现具体的审批规则 */ public abstract void handle(LeaveRequest request); }在抽象类中,我们只做了两件事:
- 维护
next引用,用于指向链上的下一个处理者。 - 声明抽象方法
handle,由具体的审批人实现。
这里的一个细节是:抽象处理者既可以是抽象类,也可以是接口。在 Java 8+ 中,接口可以包含default方法,也能实现类似的功能。但如果是审批这种“持有下一个引用”的模型,抽象类往往更直观,因为下一个节点的引用状态可以统一放在抽象类中管理。
4.4 定义具体处理者
接下来实现三个具体审批人:组长、部门经理、总经理。
组长审批:
package com.example.demo; /** * 组长审批:2天以内 */ public class GroupLeaderApprover extends Approver { @Override public void handle(LeaveRequest request) { if (request.getDays() <= 2) { System.out.println(request.getName() + " 请假 " + request.getDays() + " 天,组长审批通过。"); } else { // 超出权限范围,交给下一个审批人 if (next != null) { next.handle(request); } else { System.out.println("请假天数过多,无人审批。"); } } } }部门经理审批:
package com.example.demo; /** * 部门经理审批:2天以上,5天以内 */ 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() + " 天,部门经理审批通过。"); } else { if (next != null) { next.handle(request); } else { System.out.println("请假天数过多,无人审批。"); } } } }总经理审批:
package com.example.demo; /** * 总经理审批:5天以上 */ public class BossApprover extends Approver { @Override public void handle(LeaveRequest request) { if (request.getDays() > 5) { System.out.println(request.getName() + " 请假 " + request.getDays() + " 天,总经理审批通过。"); } else { System.out.println("流程结束,未匹配到合适的审批人。"); } } }你会发现,每个具体处理者的代码结构非常相似:先判断自己有没有权限处理;有则处理,没有则传递给下一个节点。这就是责任链模式中“每个处理者自己决定自己能否处理”的设计思想。
4.5 构建责任链并运行验证
最后写一个客户端来组装责任链。
package com.example.demo; /** * 客户端:组装责任链并发送请求 */ public class Client { public static void main(String[] args) { // 1. 创建审批人节点 Approver groupLeader = new GroupLeaderApprover(); Approver manager = new ManagerApprover(); Approver boss = new BossApprover(); // 2. 组装责任链:组长 -> 部门经理 -> 总经理 groupLeader.setNext(manager); manager.setNext(boss); // 3. 发送请求,链头是组长 System.out.println("=== 请假 1 天 ==="); groupLeader.handle(new LeaveRequest("张三", 1)); System.out.println("=== 请假 3 天 ==="); groupLeader.handle(new LeaveRequest("李四", 3)); System.out.println("=== 请假 7 天 ==="); groupLeader.handle(new LeaveRequest("王五", 7)); } }运行结果:
=== 请假 1 天 === 张三 请假 1 天,组长审批通过。 === 请假 3 天 === 李四 请假 3 天,部门经理审批通过。 === 请假 7 天 === 王五 请假 7 天,总经理审批通过。可以看到,客户端只需要把请求交给链头,至于请求最终由谁处理、处理规则是什么,客户端完全不需要关心。这是责任链模式最核心的价值。
这里有一个容易忽略的细节:BossApprover是最末端的节点,它后面已经没有next了。所以它的逻辑里不管匹配不匹配,都要给流程一个明确的结尾。实际项目中的末端节点,通常是一个兜底处理器:要么处理请求,要么抛出异常。
5. 责任链模式在主流框架中的应用
责任链模式从来不只是教科书里的理论,它在主流框架中广泛存在。理解框架里的责任链,能帮你更好地配置框架、排查问题。
5.1 Servlet Filter 过滤器链
Java Web 开发中,Filter就是一条典型的责任链。多个过滤器通过FilterChain串联起来,每个过滤器决定是否放行请求。
public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding("UTF-8"); // 放行,继续执行下一个过滤器或目标资源 chain.doFilter(request, response); } }chain.doFilter()就相当于责任链中的next.handle()。如果过滤器不调用doFilter(),请求就被拦截在当前节点。
5.2 Spring Security 的过滤器链
Spring Security 的安全机制,本质上是建立在 Servlet Filter 之上的一个大型责任链。SecurityFilterChain中包含了很多默认的Filter,比如:
UsernamePasswordAuthenticationFilter:处理表单登录;BasicAuthenticationFilter:处理 Basic 认证;ExceptionTranslationFilter:转换异常;FilterSecurityInterceptor:做最终的授权判断。
你可以通过HttpSecurity在链上任意位置插入自定义过滤器:
@Configuration public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/public/**").permitAll() .anyRequest().authenticated() .and() .addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }理解这一点对排查 Spring Security 问题非常有帮助。比如“登录接口为什么被拦截”,本质上就是责任链上的某个过滤器没有放行请求。
5.3 MyBatis 的插件机制
MyBatis 的插件(Plugin)也是基于责任链思想实现的。MyBatis 允许你在 Executor、StatementHandler、ParameterHandler、ResultSetHandler 四个核心对象上插入拦截逻辑。多个插件会组成一条链,每个插件在调用目标方法时按顺序执行。
@Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class MyPlugin implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 前置逻辑 System.out.println("执行 SQL 前"); Object result = invocation.proceed(); // 后置逻辑 System.out.println("执行 SQL 后"); return result; } }这里的invocation.proceed()就是“调用链中的下一个节点”。
5.4 Netty 的 ChannelPipeline
Netty 是一个高性能网络框架,它的ChannelPipeline与责任链模式一脉相承。每个入站或出站事件都会在 ChannelPipeline 中依次经过所有ChannelHandler。
pipeline.addLast(new HttpRequestDecoder()); pipeline.addLast(new HttpObjectAggregator(65536)); pipeline.addLast(new MyBusinessHandler());事件从第一个 Handler 开始,逐个向后传递。任一个 Handler 都可以决定事件是否继续传播。Netty 的ChannelHandlerContext.fireChannelRead()方法,就是链式传递的体现。
除了上面这几个框架,Dubbo 的过滤器机制、OkHttp 的拦截器(Interceptor)、Spring 的HandlerInterceptor、日志框架的 Appender 链,到处都能看到责任链模式的身影。
6. 责任链模式常见问题与设计误区
在实际开发中,责任链模式如果用得不好,反而会给项目带来维护负担。下面这几个问题是我见过的高频坑点。
6.1 请求没有处理者,直接石沉大海
如果在责任链的末端没有兜底逻辑,请求就不声不响地结束了。这在审批、风控等强业务场景中非常危险。
解决思路:在链路末端增加一个兜底处理者。它可以抛出异常、记录告警日志,或者返回一个默认结果。
public class DefaultApprover extends Approver { @Override public void handle(LeaveRequest request) { throw new UnsupportedOperationException("没有审批人能够处理该请求"); } }6.2 责任链出现环形引用
如果 A 的 next 指向 B,B 的 next 指向 A,请求就会在 AB 之间无限循环,最终导致栈溢出。
解决思路:构建责任链时保持单向性。不要手写环形引用,尽量通过工程手段统一构建链路。例如用列表来维护处理者,再循环设置 next,让环形引用从结构上无法出现。
6.3 处理顺序依赖构建顺序,容易出错
责任链的执行结果与链的构建顺序强相关。同一个请求,如果先经过“组长”再经过“经理”,和反过来执行,结果完全不同。
解决思路:在文档或注释中明确链路的顺序,最好在配置中心或数据库表中维护顺序,让非开发人员也能调整。
6.4 滥用责任链,把简单逻辑复杂化
一个只有两层判断的逻辑,强行套责任链,只会让代码变多、阅读变难。责任链适合处理“处理者数量可能动态变化”的场景。如果你的节点是稳定的两三个,用简单策略模式或 if else 反而更好。
判断标准:当新增一种处理者不需要修改已有代码时,才是责任链模式发挥最大价值的时候。
6.5 在责任链中修改共享请求对象
多个处理者同时修改请求对象,会导致下一节点拿到的数据和上一节点看到的不一致。这种隐式状态修改很难排查。
解决思路:请求对象尽量设计为不可变对象;如果确实需要传递中间结果,可以封装一个独立的上下文对象。
7. 责任链模式最佳实践与工程建议
结合我平时写业务代码的经验,这里分享几条责任链模式的实战建议。
7.1 用 Builder 模式构建责任链,屏蔽组装细节
当责任链节点较多时,客户端里写一串setNext()会非常啰嗦。推荐引入 Builder 模式:
public class ApproverChainBuilder { private Approver head; private Approver tail; public ApproverChainBuilder add(Approver approver) { if (head == null) { head = approver; tail = approver; } else { tail.setNext(approver); tail = approver; } return this; } public Approver build() { return head; } }客户端调用:
Approver chain = new ApproverChainBuilder() .add(new GroupLeaderApprover()) .add(new ManagerApprover()) .add(new BossApprover()) .build();这样责任链的顺序就非常直观,后续增删节点也方便。
7.2 配合 Spring 依赖注入,自动收集处理者
在 Spring Boot 项目中,你可以让所有具体处理者成为 Spring Bean,然后通过List注入自动收集:
@Component public class ApproverChainConfig { @Autowired private List<Approver> approvers; @Bean public Approver approverChain() { // 按 @Order 排序 approvers.sort(Comparator.comparingInt(this::getOrder)); return buildChain(approvers); } }这样每新增一个审批角色,只需要写一个新的Approver实现类并注册为 Bean,完全不需要修改现有代码。这正是责任链模式“对扩展开放,对修改关闭”的完美体现。
7.3 结合链路追踪,为每个处理节点记录日志
责任链的调试难点在于:你不知道请求当前到了哪个节点、在哪一步被卡住了。建议在每个处理者的handle方法里打印清晰日志,或者在请求对象中维护一个已处理节点列表。
public abstract class Approver { protected Approver next; public void handle(LeaveRequest request) { request.addTrace(getClass().getSimpleName()); doHandle(request); } protected abstract void doHandle(LeaveRequest request); }这样即使链路出问题,你也可以根据 trace 信息快速定位是哪个节点出了问题。
7.4 控制责任链长度,避免性能损耗
责任链模式虽然优雅,但不是越长越好。每个节点的判断都有开销。如果链上有几十个节点,而每个节点只做一点简单的判断,性能就会受到不必要的影响。对于性能敏感的场景,可以用“短路设计”,让前置节点快速过滤掉大部分请求,避免请求走到很深的链路。
7.5 明确每个节点的单一职责
一个处理者只做“一件事”。比如“参数校验”的节点只做校验,“敏感词过滤”的节点只做过滤。如果某个节点既要校验又要改数据还要记日志,职责就太混杂了,未来极难维护。
8. 总结
责任链模式通过将多个处理者串成一条链,解耦了请求的发送者和接收者。请求沿着链路传递,每个节点自行决定是处理请求还是转发请求。这种设计带来的最大收益是:新增处理逻辑时不需要修改原有代码,只需要新增节点并重新组装链路,符合开闭原则。
在实际开发中,责任链模式广泛应用于审批流、日志记录、过滤器、拦截器、认证授权、网关路由、插件机制等场景。Servlet Filter、Spring Security、MyBatis Plugin、Netty ChannelPipeline、OkHttp Interceptor 等框架机制中,都能看到责任链模式的影子。
只要你记住下面三点,责任链模式就能成为你工具箱里趁手的工具:
- 使用场景:处理者可能会动态变化,且请求需要被多个对象依次尝试处理。
- 核心关键:每个节点自行判断处理或转发,链的组装与请求发送解耦。
- 易犯错误:没有兜底节点、链路顺序依赖构建顺序、滥用导致代码复杂。
下一步你可以尝试把项目中的一段if else逻辑重构成责任链模式,感受一下变更成本的变化。如果这篇文章对你有帮助,欢迎点赞、收藏、转发。你的支持是我持续输出技术干货最大的动力。