☰
责任链模式实战:从if-else地狱到框架源码
2026/10/3 4:03:13 网站建设 项目流程

1. 责任链模式是什么?为什么它总出现在面试和重构清单里

写代码写久了,你一定会碰到这样一种方法:里面塞满连续if-else,比如根据请假天数判断由谁来审批,根据回调参数顺序做各种校验,根据日志级别决定谁来打印。这时候你其实已经站在了责任链模式的门口。责任链模式在 GoF 的 23 种设计模式中属于行为型模式,它把“我做判断”的过程,变成一条由多个处理者组成的链:每个节点只关心自己能不能处理,不能处理就把请求继续往下传,直到有人处理,或者链走到尽头。

这套思路在 Java 后端里到处都是。你天天用的 Servlet Filter,本质上就是一条过滤器链;Spring Security 的认证、授权、CSRF 防护,也是靠过滤器链逐层拦截;MyBatis 的插件机制更是把拦截器串起来,形成类似责任链的调用过程。甚至在写普通业务代码时,遇到审批流、登录校验、风控校验规则这类需求,责任链都能帮你把一段越长越臃肿的方法,拆成一个个独立、可插拔的节点。

它解决的核心问题,说穿了就一句话:请求的发送者和接收者解耦。发送者不需要知道最终谁会处理这个请求,只需要把请求丢给链头,后续的传递逻辑由每个节点自己负责。省去了“先判断再分支”的臃肿流程,新增处理节点的时候几乎没有侵入成本。

我常给团队里的新人打这样的比方:公司里报销审批。几十人的团队,报销 500 元以下小组长就能批,500 到 2000 元要部门经理批,超过 2000 元还得总监签字。报销单递到谁手里,谁就判断自己权限够不够,不够就往上递。这跟责任链模式一摸一样。作为报销单,你根本不需要关心领导们是怎么分级审批的,你只需要把单子交出去。

1.1 责任链模式的核心组成

责任链其实只由三个角色组成,结构非常简单:

  • Handler,抽象处理者。通常是个抽象类或接口,定义处理请求的方法,同时持有一个指向下一个处理者的引用。
  • ConcreteHandler,具体处理者。实现抽象处理者的判断逻辑,能处理就处理,不能处理就调用next继续传递。
  • Client,客户端。负责组装链并发出请求,通常只需要知道链上的第一个处理者是谁。

这里最反直觉的一点是:客户端只认识链头。整条链的内部结构,对客户端是隐形的。客户发起请求时,只需要调用链头的处理办法,剩下的事全交给链路内部流转。这也就意味着,链的构建逻辑和请求的发起逻辑可以彻底分开,比如集中写在一个buildChain()方法里,或者交给 Spring 容器去装配。

1.2 什么时候该用?什么时候不该用

责任链模式不是万能药。我总结几个信号,方便你对号入座。

适合用的场景有:处理流程固定但可能持续扩展,每个节点要求“要么处理,要么放行”。典型的就是审批流、登录校验、参数校验、敏感词过滤、日志多级格式化。还有那种一个请求在不同阶段会被不同模块处理的场景,比如工单处理、售后升级、消息分类转发。

不适合用的场景也有:处理顺序非常不稳定,比如今天你要先做 A 校验后做 B,明天反过来,责任链会让这种动态变换变得很难维护,这时候考虑策略模式或者命令模式更合适。另外一个容易忽略的点是:如果所有节点都一定要执行,而且执行顺序极其关键,也要慎用责任链。因为责任链强调的是“按需处理”,如果中途某个节点判断失误中断了链路,后面所有节点都不会执行,这个坑埋得比想象中深。

还有,链的末端必须有兜底。很多人写完责任链就不管了,结果请求走到链尾没人处理,线上静默丢单。保底做法是加一个“终结点”处理者,收到请求就抛异常或者打日志,把不可达情况显式暴露出来。别让请求悄悄地消失,这是责任链落地最基础也最重要的一条准则。

2. 从零手写一个责任链:请假审批流程

代码层面的责任链并不高深。我下面从请求对象、抽象处理者、具体处理者到链的组装,完整贴一遍。所有代码都是可以直接跑通的,建议你开个 Java 文件跟着敲一遍。

2.1 定义请求对象

责任链里传递的“请求”需要一个载体。我先定义一个请假请求:

public class LeaveRequest { private final String name; private final int days; private final 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; } @Override public String toString() { return name + " 请假 " + days + " 天,原因:" + reason; } }

我建议把请求对象设计成纯数据载体,别往里面塞业务逻辑。它可以携带状态,比如“当前处理到了哪一级”“累计处理了多久”,但不要让它自己去调用审批逻辑。责任链的核心变化点是处理者,把行为都堆在请求上面,等于又把代码耦合回去了。

2.2 抽象处理者设计

抽象处理者是整个模式的骨架。最常用的写法是抽象类持有下一个节点引用:

public abstract class Approver { protected String name; protected Approver next; public Approver(String name) { this.name = name; } public Approver setNext(Approver next) { this.next = next; return next; } public abstract void handleLeave(LeaveRequest request); }

setNext返回next是为了支持链式组装。看到这里你可能会疑惑,为什么返回的是next而不是this?因为链式调用的时候,我们希望表达的是“a 的下一个节点是 b,b 的下一个节点是 c”。如果用返回this的链式写法:

leader.setNext(manager).setNext(director);

第一次调用把leader.next设成manager,第二次调用又把leader.next覆盖成director,链就断了。所以返回next更符合组装链的语义。如果你怕用错,也可以不用链式,直接分行写:

leader.setNext(manager); manager.setNext(director);

这样最稳妥,可读性也不差。

2.3 编写具体处理者:能批就批,不能批就往下传

我需要三个具体节点:组长、经理、总监。

public class TeamLeader extends Approver { public TeamLeader(String name) { super(name); } @Override public void handleLeave(LeaveRequest request) { if (request.getDays() <= 2) { System.out.println(name + " 审批通过:" + request); } else if (next != null) { next.handleLeave(request); } } }
public class Manager extends Approver { public Manager(String name) { super(name); } @Override public void handleLeave(LeaveRequest request) { if (request.getDays() > 2 && request.getDays() <= 7) { System.out.println(name + " 审批通过:" + request); } else if (next != null) { next.handleLeave(request); } } }
public class Director extends Approver { public Director(String name) { super(name); } @Override public void handleLeave(LeaveRequest request) { if (request.getDays() > 7) { System.out.println(name + " 审批通过:" + request); } else { System.out.println("请假天数异常,无人能审批"); } } }

在Director里我主动加了一个兜底逻辑,链走到尾仍处理不了就给出提示。生产环境里,这里应该直接抛出业务异常,比如new IllegalArgumentException("请假天数异常,超出所有审批权限"),而不是像示例这样静默打印。记住:责任链的“链尾无人处理”是一条错误路径,必须让调用方感知到。

2.4 客户端组装链并发出请求

客户端代码特别简单:

public class ApprovalClient { public static void main(String[] args) { Approver leader = new TeamLeader("张组长"); Approver manager = new Manager("李经理"); Approver director = new Director("王总监"); leader.setNext(manager).setNext(director); System.out.println("--- 场景1:请假 1 天 ---"); leader.handleLeave(new LeaveRequest("小明", 1, "家中有事")); System.out.println("--- 场景2:请假 5 天 ---"); leader.handleLeave(new LeaveRequest("小红", 5, "婚假")); System.out.println("--- 场景3:请假 10 天 ---"); leader.handleLeave(new LeaveRequest("小刚", 10, "家里盖房")); } }

运行结果:

--- 场景1:请假 1 天 --- 张组长 审批通过:小明 请假 1 天,原因:家中有事 --- 场景2:请假 5 天 --- 李经理 审批通过:小红 请假 5 天,原因:婚假 --- 场景3:请假 10 天 --- 王总监 审批通过:小刚 请假 10 天,原因:家里盖房

这段代码的扩展点非常清楚:以后想加“老板批 30 天以上”的节点,只需要新增一个Boss类,然后在链路组装处插入两步:

Boss boss = new Boss("赵老板"); manager.setNext(boss); boss.setNext(director);

已存在节点的代码都不用动,完全符合开闭原则。这也是责任链模式最直观的价值。

3. 纯责任链与非纯责任链:一小步设计却决定整套行为

很多人写责任链写了一阵子,根本不知道这模式还分两种流派,结果设计出来的链路行为和预期完全不一样。

3.1 纯责任链:一单到终点,只处理一次

“纯”责任链的意思是:整个链路里,只有一个节点能够处理这个请求。一旦某个节点处理成功,链路立即终止,后面节点不再运行。上面请假审批的例子就是纯责任链。

纯责任链适合“独占判断”场景,比如找客服解决一个问题,一旦 A 客服给你处理了,这条工单就不用在另一个人那边重复处理一遍。再比如日志框架里,一条日志如果被 info 级别节点接收,就不会再被 debug 级别节点处理。

纯责任链有一个很容易踩的坑:处理者之间的条件必须互斥并且覆盖全场景。假设组长条件写成<= 3,经理条件也写成<= 7,那3 <= 天数 <= 7的请求永远会被组长截胡,经理节点形同虚设。所以纯责任链里的边界值一定要前后对齐,建议在代码里用常量统一管理,避免魔法数字。

3.2 非纯责任链:每个节点都参与,也能随时中断

“非纯”责任链允许同一个请求经过多个处理者,每个处理者可以先把该做的事情做完,再继续传递。最经典的例子就是 Servlet 里的 Filter 链。

我用伪代码随手写一个:

public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { if (!checkLogin(request)) { response.getWriter().write("未登录,拒绝访问"); return; } chain.doFilter(request, response); } }

过滤器并不要求只被一个人处理。登录校验过滤器拒绝未登录请求,日志过滤器记录请求轨迹,权限过滤器校验角色,这些过滤器之间可以“叠加”执行。每个节点都有机会处理,也可以选择直接放行。这是最典型的非纯责任链。

非纯责任链最烦人的地方是next漏调。你写了一个权限过滤器,判断用户有权限后加了一堆业务逻辑,结果最后忘记调chain.doFilter(),请求就卡死在这个过滤器里,接口一片空白。我曾经排查过一个线上问题,几个人的日志和接口都正常,就是请求不返回,最后发现是某个过滤器在if分支里少了一行chain.doFilter。

3.3 设计时如何选择

维度纯责任链非纯责任链
处理者数量最终只有一个处理者生效可有多个处理者依次执行
请求中断方式处理成功即停止节点主动不调用next才停止
典型例子审批流、日志级别匹配Servlet Filter、Spring MVC Interceptor
实现注意点条件必须互斥且完整覆盖时刻小心漏调next导致链路断裂

面试时把这个意思讲明白,面试官基本能确定你是真的懂责任链,而不只是背了定义。用一句话概括:纯责任链是“谁能处理谁上”,非纯责任链是“大家轮流上,谁想停谁就停”。

4. 实战重构:把 If-Else 地狱改成责任链

理论看一百遍,不如动手改一个真实代码。这节我用一个很常见的场景:第三方支付回调的风控校验,演示怎么把一长串 if-else 重构成责任链。

4.1 重构前的代码长什么样

假设系统接入支付回调,需要对回调报文做四项校验:签名校验、金额校验、账户状态校验、风控规则校验。

public class CallbackService { public void handleCallback(CallbackRequest req) { if (!checkSign(req)) { throw new IllegalArgumentException("签名不合法"); } if (req.getAmount() <= 0 || req.getAmount() > 100000) { throw new IllegalArgumentException("金额不在允许范围"); } if (!checkAccountStatus(req.getAccountId())) { throw new IllegalArgumentException("账户状态异常"); } if (!checkRisk(req)) { throw new IllegalArgumentException("触发风控规则"); } // 真正处理业务 doBusiness(req); } }

看起来还挺清晰?问题在于,每加一个校验规则,这个方法就变得更长。更麻烦的是,校验逻辑往往被复制到不同入口:前端创建订单、后台补单、渠道重推,各种入口都调这段。一旦某个入口漏加了校验,线上就可能会产生一笔不该成功的订单。这种情况下,任何新需求都让人如临大敌。

4.2 用责任链重构整个校验流程

我为回调校验设计一个专门的校验链,每个校验器是链上的一个节点。

先定义链上传递的上下文:

public class CallbackContext { private CallbackRequest request; private Object bizData; public CallbackRequest getRequest() { return request; } public void setRequest(CallbackRequest request) { this.request = request; } public Object getBizData() { return bizData; } public void setBizData(Object bizData) { this.bizData = bizData; } }

抽象校验器:

public interface CallbackValidator { CallbackValidator setNext(CallbackValidator next); void validate(CallbackContext context); }

实现签名校验:

public class SignValidator implements CallbackValidator { private CallbackValidator next; @Override public CallbackValidator setNext(CallbackValidator next) { this.next = next; return next; } @Override public void validate(CallbackContext context) { if (!checkSign(context.getRequest())) { throw new BusinessException("签名不合法"); } if (next != null) { next.validate(context); } } private boolean checkSign(CallbackRequest request) { // 真实签名校验逻辑 return true; } }

其余三个校验器按同样的模式实现一遍。然后在构造回调服务时集中装配链条:

public class CallbackService { private final CallbackValidator validatorChain; public CallbackService() { SignValidator sign = new SignValidator(); AmountValidator amount = new AmountValidator(); AccountStatusValidator status = new AccountStatusValidator(); RiskValidator risk = new RiskValidator(); validatorChain = buildChain(sign, amount, status, risk); } private static CallbackValidator buildChain(CallbackValidator... validators) { for (int i = 0; i < validators.length - 1; i++) { validators[i].setNext(validators[i + 1]); } return validators[0]; } public void handleCallback(CallbackRequest req) { CallbackContext context = new CallbackContext(); context.setRequest(req); validatorChain.validate(context); doBusiness(context); } }

重构之后,handleCallback退化成两行,新的校验规则即插即用。我特意用buildChain这个静态方法代替分散的链式赋值,目的就是把链条的组装过程集中到一个地方,避免链头被无意覆盖的尴尬问题。

4.3 重构后的可维护性收益

  • 新增校验节点不需要改动已有服务方法,只改链路组装处。你在线上加一个“黑名单校验器”,业务入口完全不用动。
  • 每个校验器的单元测试很容易写。以前要测一个完整校验流程,你得从最外层穿透重重 if 分支;现在直接构造CallbackContext,调用任意一个validate就能测。
  • 可以按不同调用方组装不同链路:渠道 A 不风控,渠道 B 加强风控,这在原来 if-else 里很难做。把“校验动作的编排”从“业务实现”里剥离出来,就是责任链模式的杀手锏。

这段重构代码里还藏着一个关键习惯:不要在用buildChain(sign, amount, status, risk)这样的可变参数时,把返回结果误认为最后一个节点。我见过有人图省事,直接写:

validatorChain = sign.setNext(amount).setNext(status).setNext(risk);

因为setNext返回的是next,这个表达式的最后结果是risk,于是整条链的入口变成了最后一个校验器,前面的全部失效。这也是我为什么强烈建议用buildChain参数方法或逐行赋值的原因。

5. 责任链模式在主流框架中的应用

面试题里经常出现“说出责任链模式在框架中的应用”,这个问题答好了真的加分,因为它考察的是你读源码的深度。

5.1 Servlet Filter:底层就串了一条链

不管用 Spring Boot 还是原生 Servlet,Java Web 开发里到处都有 Filter 的身影。请求从 Tomcat 进入后,并不是直接到达 Servlet,而是先经过注册好的 Filter 链。每个 Filter 通过FilterChain.doFilter()把请求继续往下传。这个模型的本质,就是责任链模式。

Spring Security 就是基于 Filter 链演化出来的。它把“认证”“授权”“CSRF 防护”“会话管理”这些安全逻辑注册为不同的 Filter,层层过滤,最终才放行到你的 Controller。如果你去看 Spring Security 的源码,会看到FilterChainProxy管理着一条VirtualFilterChain,本质上就是对 Servlet 原生 Filter 链的包装扩展。你在它上面加一个安全过滤器,就是在链上插一个节点。

5.2 Spring MVC 的 Interceptor:责任链思想的又一种表达

Spring MVC 的HandlerInterceptor分前置、后置、完成三个阶段。你可以把它理解成一个机会更多、左右都能扩展的非纯责任链。

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { Object user = request.getSession().getAttribute("user"); if (user == null) { response.sendRedirect("/login"); return false; } return true; } }

preHandle返回false的那一刻,链路后续节点就不会再执行,这不就是“处理者主动中断”吗?多个拦截器按注册顺序串起来,本质上就是用代码搭出的审核队伍。登录拦截器判断完,权限拦截器继续判断,两者互不干扰,职责完全分离。

5.3 插件体系:MyBatis Plugin 的“责任链 + 代理”

MyBatis 的拦截器可以让开发者拦截 Executor、StatementHandler、ParameterHandler 等核心对象的方法调用。它内部通过Plugin.wrap()逐层包装目标对象,把多个拦截器串成一条链路。调用请求先经过最外层拦截器,层层剥进核心执行逻辑。这实际上是责任链思路和动态代理的结合使用,只不过每个节点不是直接持有另一个节点的引用,而是通过 JDK 动态代理层层代理。

你不需要背这些框架源码,但面试时能说出来 Filter、Interceptor、MyBatis Plugin 三者都体现责任链,已经足够证明你理解“请求沿着处理链传递”这种组织方式。如果再能补充一句“Spring Security 是 Filter 链的加强版”,这个印象分会更高。

6. 责任链模式 vs 装饰器 vs 策略:别再把它们混为一谈

模式之间的边界经常让新人头大。责任链和装饰器在结构上有点相似,都是一条链式引用,但意图完全不同。

6.1 责任链 vs 装饰器

装饰器模式的核心是“给一个对象动态增加功能”,它处理的是能力的升级。责任链的核心是“寻找合适的处理者”,它处理的是归属的分配。

责任链是请求在多个独立处理者之间传递,装饰器是调用在同一个对象的包装器之间嵌套。对方法调用来说,装饰器每次调用都会走进所有层;责任链则可能在中途某个节点戛然而止。

用大白话对比:装饰器像路上层层加码的补给站,每一层都应该做点什么;责任链像传桶游戏,桶到手里,你决定自己接住,还是传给下一个人。结构相似,行为意图南辕北辙。

6.2 责任链 vs 策略

策略模式把一组可以相互替换的算法封装起来,由客户端决定用哪个算法执行。它解决的问题是“同一件事有不同做法”。责任链解决的是“同一件事不同节点分级处理”。

打个比方:搜索排序既可以用按时间排序策略,也可以用按热度排序策略,这是策略模式。用户反馈消息先由客服判断,客服处理不了升级给技术,技术处理不了升级给主管,这是责任链模式。策略模式是换算法,责任链模式是换处理人,边界其实很清楚。

6.3 什么时候组合使用

实践中,责任链节点内部也可以组合使用策略。比如风控校验节点里,对不同类型的支付渠道使用不同的规则策略。模式并不是孤立的,一条链上的每个节点可以再套一层策略选择器。

重要的是别背定义,要读懂这段代码想要表达的变化点在哪里。如果变化点是“谁来处理”,用责任链;如果变化点是“用什么方式处理”,用策略;如果变化点是“处理能力如何增强”,用装饰器。对着变化点选模式,再也不用纠结。

7. 常见坑、面试套路与我的实战经验

这部分是长期踩坑后的心血。代码之外的细节,往往决定了一个设计模式能不能真正落地。

7.1 六大常见坑

  • 链上节点条件互斥没做好。纯责任链里两个节点条件重叠,请求会被前面节点截胡,后面的节点永远跑不到。在写纯链时,条件必须是严格的区间边界,像<= 2和> 2 && <= 7这种写法,边界值必须反复对齐。
  • 忘了兜底节点。请求走到链尾可能没有处理者,线上会静默丢单。我建议每个责任链都保留一个兜底处理者,要么抛异常,要么打日志,让不可达情况显式暴露。
  • 链头被无意覆盖。链式setNext时,只要出现链里有多个节点的场景,就要特别小心最后拿到的是不是链头。宁可多写几行,也不要炫技。
  • 漏调next。非纯责任链里漏调next会让请求断掉。所有放行路径都要走到next,最好的办法是节点内部总是先判断能否处理,不能处理就直接next,把所有分支都收敛到一行调用。
  • 链路过长。一条责任链接一百个节点,可读性会被反噬。超过七八个节点,我建议做分组,把链路拆成子链,再用“复合处理者”把子链串起来,否则排一次错会查到怀疑人生。
  • 把责任链填成命令链。如果每个节点必须无条件执行,且没有终止判断,那它不是责任链,更适合用命令模式或管道模式。模式用错,比不用模式更难受。

7.2 面试如何聊责任链

面试官问“聊聊责任链模式”,建议按这个节奏答:

  • 先一句话说清定义:多个处理者形成链,请求沿链传递,每个节点决定自己处理还是交给下家。
  • 再描述三个参与者:抽象处理者、具体处理者、客户端,强调客户端只知道链头。
  • 说一个真实样例,比如请假审批流、过滤器的doFilter,重点讲清楚“能处理就处理,不能处理就传递”。
  • 补一句两种变体的核心区别:纯责任链只处理一次,非纯责任链可以叠加执行。
  • 再提到它在 Spring Security、MyBatis Plugin 等框架中的应用。
  • 结尾讲一个踩坑故事,比如漏调next导致请求卡死,这个回答就完整了。

面试官真正想听的是“你能否在合适场景用出合适设计”,而不是设计模式的八股定义。故事和细节比术语更值钱。

7.3 我的几条实战体会

第一,责任链模式最适合用来对付“增长中的规则列表”。如果你发现一个新需求来了,第一反应是打开某个方法再复制一个if,这就是责任链该出现的位置。别等 if-else 堆到二十层才动手,规则到了三个以上就应该考虑抽链。

第二,代码评审里看到超过三个连续相同结构的 if 分支,我一般建议同事把这段拆成责任链。不是为了优雅,而是为了后续扩展的时候不互相踩脚。在一个方法里加条件永远是最简单的,但维护的人每次都要把所有条件重新想一遍。

第三,记得给链上节点取一个好名字。命名要贴着业务角色走,用RiskValidator、SignValidator、LoginInterceptor这种具体名,不要用Handler1、Handler2。读代码的人一眼就知道节点管哪段业务,排查问题的时候能少走很多弯路。

模式不是炫技工具。责任链的本质是纪律:每个节点做自己能做的事,把不能做的事体面地交给下一个人。如果你能在自己的项目里试着用一次,再回来读这段文字,应该会有更深的体感。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询