资讯动态

责任链模式实战:从if-else地狱到框架源码

发布时间:2026/10/3 4:03:22 来源:尧图企业网站定制
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 的下一个节点是 bb 的下一个节点是 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 还是原生 ServletJava 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。读代码的人一眼就知道节点管哪段业务排查问题的时候能少走很多弯路。模式不是炫技工具。责任链的本质是纪律每个节点做自己能做的事把不能做的事体面地交给下一个人。如果你能在自己的项目里试着用一次再回来读这段文字应该会有更深的体感。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑