尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

责任链模式全解析:概念、Java实现与实战避坑

责任链模式全解析:概念、Java实现与实战避坑 碰过权限系统的同学大概率都写过这种代码一个请求进来先判断是不是管理员不是就返回再判断是不是VIP是就走VIP通道再判断是不是白名单用户……每个版本都在原来的判断上加一个分支改到最后方法体比老太太的裹脚布还长。我第一次被责任链模式点醒就是在重构一套支付风控模块的时候那段代码里黑名单、额度、频次、设备指纹全堆在四五个嵌套if-else里每次新增规则都得从上往下一行行看还担心漏了哪个return。后来我按责任链模式重新梳理把每个校验拆成一个独立节点请求像走传送带一样一路传下去能处理就处理不能处理就交下一站。那一次重构之后我才意识到23种设计模式里责任链是被低估得很厉害的一个。这篇文章我打算把它讲透。不管你是准备期末考试、软考还是在真实项目里想落地这个模式都能用得上。我会先用一个生活化的例子把核心概念讲清楚再给一套能直接跑的Java代码把它和装饰器、状态、策略这些容易混淆的模式放到一起对比最后补上软考速记和几个实战中特别容易踩的坑。看完这篇你应该能自己画清楚一张责任链的地图。1. 责任链模式是什么从“请假审批”入门核心概念1.1 责任链模式的定义与角色构成责任链模式Chain of Responsibility Pattern属于GoF二十三中设计模式里的行为型模式它的定义用一句话总结就是把多个可能处理同一请求的对象串联成一条链请求沿着这条链向后传递链上的每个对象依次判断自己有没有能力处理这个请求能处理就处理处理不了就交给下一个对象。定义里面最核心的两个词是“串成一条链”和“依次判断”。为什么要强调这两个词因为很多初学者以为责任链模式就是一个链表把对象一个个连起来而已。链表只是它的实现载体真正重要的是它把“谁来处理这个请求”的决策权从请求发送者手里剥离了出去。发送者根本不需要知道这条链上有多少个节点、每个节点是谁它只需要把请求丢给链头后面的事就跟发送者无关了。责任链模式里通常有三个角色抽象处理者Handler一般是一个抽象类或者接口定义处理请求的方法同时持有下一个处理者的引用。具体处理者ConcreteHandler实现抽象处理者负责判断自己能否处理请求不能处理就把请求向后传递。客户端Client负责组装责任链并把请求发送给链上的第一个处理者。有些教材还会单列一个“请求对象”的概念比如请假天数、报销金额、订单信息它承载了链上各个节点做判断所需的数据。这个概念不复杂但考试画类图的时候经常会漏掉我特意在这里提一句。1.2 为什么拿“请假审批”当入门例子因为责任链模式最典型的业务场景就是审批流。假设你在公司做请假系统审批规则大概是这样的请假3天以内组长批5天以内主管批超过7天要经理批再往上可能要总监。这些规则不是一成不变的可能哪天下文件说“所有超过10天的假必须经过人力复核”你就要改逻辑。如果不用责任链模式你大概率会写成这样一个方法里从上到下塞满if-else每个if对应一个审批人然后根据请假天数决定走哪个分支。表面上看也能跑但每加一条规则就要动这个核心方法测试要全部回归一遍代码也会越来越臃肿。用责任链模式之后组长、主管、经理、总监各自实现一个Handler节点按顺序把后继者串起来。提交一个15天的请假申请组长发现自己批不了把请求传给主管主管也批不了继续传给经理经理能批就由经理审批并结束。将来新增加一个人力复核节点只需要在链尾挂一个新节点不用去修改任何旧节点。到这里责任链模式的价值就很清楚了责任边界清晰。每个节点只负责自己职责范围内的判断不需要关心别人。发送者与接收者解耦。客户端只认识链头其他节点对它完全透明。链可以动态组装。增加、减少、调整节点顺序都是局部改动。符合开闭原则。新增处理者不需要改动旧代码只是往链上多加一个节点。不过这种模式从结构上就注定了它也有成本比如链路太长时调试麻烦、节点无状态要求更高这些细节我留到后面专门用一节来聊。2. 用Java手写责任链模式核心代码骨架与两种组装方式2.1 抽象处理者与具体处理者的标准写法下面这份Java代码是责任链模式最经典的教学实现也是我在实际项目里见过最多的雏形。它的设计思路非常朴素每个节点能处理就处理处理不了就往后传。先定义抽象处理者public abstract class Handler { protected Handler successor; public void setSuccessor(Handler successor) { this.successor successor; } public abstract void handleRequest(int days); }这里的successor字段就是“后继节点”setSuccessor方法用来设置它。之所以用protected是为了让子类能直接访问这个字段去判断后面还有没有节点。接着定义具体处理者我还是沿用请假审批的例子public class TeamLeaderHandler extends Handler { Override public void handleRequest(int days) { if (days 3) { System.out.println(组长审批通过请假天数 days); } else if (successor ! null) { successor.handleRequest(days); } else { System.out.println(无人可处理请求被丢弃); } } } public class ManagerHandler extends Handler { Override public void handleRequest(int days) { if (days 7) { System.out.println(主管审批通过请假天数 days); } else if (successor ! null) { successor.handleRequest(days); } else { System.out.println(无人可处理请求被丢弃); } } }经理、总监这两个类结构完全一样只是把阈值改掉我就不重复贴了。注意看这个“接力赛”模型每个处理者只有两种走法能处理就直接处理并结束不能处理就调用successor.handleRequest把接力棒交给下一棒。如果传到链尾都没人处理就必须给请求一个明确归宿——返回失败、抛异常、进兜底逻辑绝对不能什么都不做就消失了。这个约定非常关键后面讲坑的时候我会再展开。这里有个初学者特别容易忽略的点责任链并不强制要求“只能有一个节点处理请求”。如果某个节点处理完之后还想让后面的节点也处理一遍那它就不能直接return而是要在处理完自己的逻辑之后继续显式调用successor.handleRequest。这种“每个节点都处理”的变体在Servlet过滤器和Netty管道里非常常见。换句话说责任链模式是一种框架至于一个请求是被一个节点消费还是被多个节点接力处理完全取决于你的具体实现。2.2 客户端如何组装并启动责任链组装责任链最直观的方式就是在客户端把节点一个个拼起来public class Client { public static void main(String[] args) { Handler teamLeader new TeamLeaderHandler(); Handler manager new ManagerHandler(); Handler director new DirectorHandler(); teamLeader.setSuccessor(manager); manager.setSuccessor(director); int[] applyDays {1, 5, 15}; for (int days : applyDays) { System.out.println(提交请假申请 days 天); teamLeader.handleRequest(days); } } }运行结果提交请假申请1天 组长审批通过请假天数1 提交请假申请5天 主管审批通过请假天数5 提交请假申请15天 总监审批通过请假天数15这条链隐含了一个重要约定客户端永远只把请求发给链头。如果客户端心血来潮直接调用了manager.handleRequest那整个链条的语义就被破坏了——组长节点被跳过后续审批记录、规则判断全部错乱。设计责任链的时候一定要把这个约定用注释或者文档明确出来别让团队里的人随手绕过链头。2.3 两种常见的组装风格手动拼接与链式风格市面上把责任链组装成链的方式有两种主流的写法。第一种就是上面那种通过setSuccessor逐段拼接。优点是直观、兼容性强适合团队里风格偏保守的老项目。缺点是链特别长的时候客户端里会出现一大串setSuccessor看着不优雅。第二种是链式风格让setNext方法返回下一个节点这样组装的时候可以一气呵成public abstract class Handler { private Handler next; public Handler setNext(Handler next) { this.next next; return next; } public void handleRequest(int days) { if (canHandle(days)) { doHandle(days); } else if (next ! null) { next.handleRequest(days); } else { System.out.println(无人可处理请求被丢弃); } } protected abstract boolean canHandle(int days); protected abstract void doHandle(int days); }子类只需要实现canHandle和doHandle把“要不要处理”和“具体怎么处理”这两件事分开。组装链的时候可以这样写Handler chain new TeamLeaderHandler() .setNext(new ManagerHandler()) .setNext(new DirectorHandler()); chain.handleRequest(5);这种写法把模板代码收敛到了抽象类里具体处理者看起来更清爽而且从读代码的角度看链的形态非常直观。我个人更推荐这种写法因为它把“责任链怎么走”和“每个节点做什么”分离开了后面的节点不再需要关心successor ! null这类分支。还有一种基于ListHandler的实现我也提一嘴。它把所有处理者放在一个ArrayList里然后用for循环从头到尾遍历遇到第一个能处理的就处理。这种写法不是严格意义上的链表结构但它天然支持“多个节点依次处理”也方便统一打日志。如果你的需求没有那么强调“链式传递”的语义用列表遍历反而更可靠。这也是我想特别强调的观点责任链模式的本质是让每个节点决定“处理”还是“传递”它到底用链表还是用数组实现其实属于次要问题。设计模式服务的是业务逻辑不是反过来让业务去贴合某种死板的代码形态。3. 责任链模式和易混淆模式的分界装饰器、状态、策略我在面试别人的时候发现一个很有意思的现象能画出责任链类图的候选人不少但能说清楚它和装饰器、状态、策略这几个模式区别的候选人很少。这几个模式在代码结构上确实有点像但它们的意图完全不同。我把这组对比讲透你以后不管是做设计题还是看框架源码都不容易模糊。3.1 责任链模式 VS 装饰器模式这两个模式是最容易被混为一谈的因为它们都长得像“一层套一层”而且都可能在类里持有一个自类型引用。区别在于意图。责任链模式的意图是筛选责任承担者。链上的每个节点都可以拍板说“到此为止我来处理”也可以说“这事我办不了传给下一家”。请求在某个环节被处理后链路可能就断了。装饰器模式的意图是动态扩展功能。每个装饰器都要执行自己的横切逻辑然后继续调用被包装对象的方法最终一定要执行到最核心的对象。装饰器链上不存在“我处理不了把它扔掉”这种事。我举个两个场景对比一下责任链模式像医院分诊台。你描述自己的症状分诊护士判断你可以去内科就直接把你转到内科后面的骨科、外科就不再参与了。如果护士拿不准才会把你往下一个科室传。装饰器模式像给汉堡加料。面包胚是核心对象生菜、番茄、牛肉、芝士是一层层装饰器。每一层都实实在在地改变并加强了汉堡但不管加多少层最后你都会拿到一个完整的汉堡不会出现“牛肉觉得芝士不符合标准把汉堡扔了”这种奇怪逻辑。用代码上的关键特征去记更准确责任链里是“不能处理就向后转”装饰器里是“处理完再继续转发”。最核心的区别是责任链允许中断装饰器不允许。3.2 责任链模式 VS 状态模式状态模式也要处理“当前状态处理完之后转移到下一个状态”的语义所以它和责任链也有一定迷惑性。我的区分标准很简单就看谁负责发起转移。责任链模式里链的整体结构是外部提前组装好的。每个节点只回答“我能不能处理”处理不了就把请求传给固定好的下游节点自己不需要操心“下一站是谁”。状态模式里状态对象内部维护转移逻辑。当前状态处理完一个事件后它会根据上下文自行决定接下来进入哪个状态。状态机不一定是单向走到底的它可能是个环可能从等待支付变成已支付又因为订单取消变回已关闭状态之间可以跳跃、可以回退。用一句话记住责任链是把链背在身上节点只需要判断要不要传给下游状态模式是把地图拿在手里每个状态都知道自己要往哪里去。如果一段业务流程的状态迁移十分清晰比如订单“待支付 - 已支付 - 已发货 - 已完成”优先考虑状态模式。如果是“同一份请求可能被不同角色逐一拦截或审批”优先考虑责任链模式。3.3 责任链模式 VS 策略模式策略模式解决的是算法替换问题。它把一组可以互相替换的算法封装起来客户端在运行时选择一个算法来用。策略模式是一次性的选定策略之后整个任务直接做完不存在“A策略处理完传给B策略”的概念。责任链模式解决的是责任划分与传递问题每个节点通常是独立的规则或流程段一条请求最后归属到链上的某个节点。再打一个比方策略模式好比打车时你选了“快车”还是“顺风车”选定之后整个行程就由这一个策略完成责任链模式好比你在医院按挂号、分诊、检查、开药一步步走每站都有可能处理你也可能把你转给下一站。面试答题的时候可以这样抓关键题干里出现“多个处理器”“按顺序尝试”“有一个能处理就行”这些词大概率是责任链出现“可互换”“运行时替换算法”这些词大概率是策略模式。3.4 一张对比表理清边界为了方便复习我整理了一张对比表建议截图保存。期末考、软考、面试前扫一眼很有用。对比维度责任链模式装饰器模式状态模式策略模式所属类别行为型结构型行为型行为型核心目的请求沿链传递直到有人处理动态增强对象功能不同状态下的行为切换算法自由替换请求是否可能中断会中断不会中断取决于状态定义执行完即结束谁持有“Next式”引用持有后继处理者持有被包装对象持有上下文对象持有策略接口调用顺序可长可短动态确定固定叠加状态自行转移只调用一次4. 真实项目里谁在用责任链从审批流到框架过滤器责任链模式不只是教科书里的习题它其实被大量地用进了各种开源框架和大型业务系统。搞懂这些真实场景你写代码的时候就不会觉得它高不可攀。4.1 审批流与风控规则链业务系统里最常见的是审批流我刚开头提到的支付风控模块就是这类场景。风控代码不重构前基本长这样public void payRiskCheck(PayRequest req) { if (!blackListService.check(req)) { return; // 黑名单拦截 } if (!quotaService.check(req)) { return; // 额度不足 } if (!frequencyService.check(req)) { return; // 频次超限 } // 设备指纹...... }当校验规则从三个变成五个、七八个的时候这个方法的复杂度会爆炸。更可怕的是各校验之间还有隐含的先后关系黑名单不过就不要再看频次额度不够就不需要再查设备。这些规则全部写在一个方法里阅读成本极高改一个校验都可能误伤另一个。用责任链重构后每个校验器实现同一个RiskHandler接口节点内部只保留自己的规则链的顺序交给配置层决定。新增一个规则就是新增一个Handler实现并挂到链上老代码一行都不用动。这才是责任链在生产环境里的最大价值。4.2 框架中的过滤器链与管道很多人早就用过责任链模式只是自己没意识到。Java Servlet规范里的FilterChain就是典型的链式处理。请求从客户端进来必须先穿过一串Filter才能到达Servlet。Spring MVC里的Interceptor也是如此多个拦截器按顺序执行本质上就是一条链。Netty的ChannelPipeline更是把这套玩到极致入站事件、出站事件都沿着pipeline里的Handler链一路传递。以Servlet的Filter为例核心逻辑是public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { // 请求处理前的逻辑 chain.doFilter(request, response); // 请求处理后的逻辑 }FilterChain会依次调用每一个过滤器每个过滤器既处理又继续传递。这里注意Servlet过滤器其实属于“每个节点都处理”的变体而不是教科书里那种“第一个能处理的节点处理完就停”的模式。所以它更贴近管道模式。这也提醒我们跟在真实框架里的模式往往会变形读源码时不要死抱着某本书的定义不放。4.3 内容审核、消息路由与日志处理内容审核是另一个高频场景。一条用户发布的文本通常要经过敏感词过滤、垃圾广告识别、图片URL检测等多个节点任何一个节点判定有问题就可以直接拦截。这里“拦截”相当于责任链里的“处理”“放行”相当于“传给下一站”。自研日志框架也常会设计成这种结构。比如日志事件从Logger出发经过Filter、Formatter最后到达Appender中间任何一层都可以决定“不再向下一层传递”。消息路由同样可以这样做。一个消费者接收到消息之后先判断消息类型是不是订单消息不是就传下去再判断是不是支付消息不是再传。通过责任链可以把路由规则和具体消费逻辑完全解耦后续新增消息类型只需要新增一个Handler节点。4.4 在Spring工程里优雅地装配责任链如果你的项目用了Spring框架我完全不建议在客户端手工做一串setSuccessor。更优雅的方式是把所有Handler节点交给Spring容器管理然后用ListRiskHandler自动注入再通过注解或排序器给这些节点排好顺序。这种以“列表遍历”方式实现的“责任链”在实际工程里往往比手写链表更可靠也更好调试。下面是一个简化的例子Component public class BlacklistHandler implements RiskHandler { Override public boolean handle(PayRequest req) { return !blacklistService.contains(req.getUserId()); } }组合类按顺序执行所有处理器Component public class RiskHandlerChain { private final ListRiskHandler handlers; public RiskHandlerChain(ListRiskHandler handlers) { // 通过 Order 注解或者自定义排序把 handlers 排好序 this.handlers handlers; } public boolean doChain(PayRequest req) { for (RiskHandler handler : handlers) { if (!handler.handle(req)) { return false; // 某个节点拦截整条链结束 } } return true; } }这种方式虽然在结构上少了“后继者”这个属性但它本质上依然遵循责任链的核心思想每个节点负责判断请求沿着处理器列表逐一传递任意一个节点可以终止链路。Spring容器负责装配业务方只需要安心实现自己的Handler逻辑。5. 软考速记与面试高频问答好多读者都在准备软考或者期末我单独开一节聊聊速记和面试。5.1 责任链模式的速记口诀和考点责任链模式的速记我的整套体系可以压缩成一句口诀“一个请求一条链各级节点轮流看能处理就处理不能处理传下站。”再配上一个结构口诀“三角色一条链Handl自关联。”“三角色”指的是抽象处理者、具体处理者和客户端“Handl自关联”是指抽象Handler里有一个指向自己的属性这就是successor字段画UML类图的时候这个自关联箭头是最显眼的特征。软考中级软件设计师科目里责任链模式经常会跟其他行为型模式混在一起做识别题。考试时如果题干出现“多个对象都有机会处理请求”“请求沿链传递”“接收者被解耦”这些词基本就可以锁定责任链。如果题目要求你从23种模式里挑出一个行为型模式责任链也几乎必在候选名单里因为它是行为型中非常典型的存在。答题还有一个顺序上的技巧先答“它是行为型模式”再答“它用来解耦请求发送者和接收者让请求沿着链传递”最后画类图补上“Handler持有自关联引用具体处理者继承Handler”这四板斧下去分数基本就能拿稳。5.2 面试官最常追问的四个问题面试里责任链的高频追问我整理了一下附上参考回答的思路。问题一如果链走到最后都没人处理请求会怎么样参考思路严格设计的责任链一定要有兜底逻辑。可以放一个兜底Handler专门处理没人认领的请求也可以直接标记失败并结束。最怕的是什么都不做让请求静默消失这种Bug排查起来非常痛苦。问题二责任链模式和管道模式有什么区别参考思路两者结构相似但语义不同。管道模式强调“每一步都要执行并把数据加工后传给下一步”责任链模式更强调“找到合适的那个处理者就停下来”。很多框架里的FilterChain实际上是管道模式的变体但因为责任链更出名大家日常会混着叫。问题三责任链模式有什么缺点参考思路链路过长时请求传递开销会变大而且各个环节分散在不同类里调试时要串联多个节点才能看清一条完整路径。另外如果Handler有状态多线程环境容易出问题。这也是为什么生产里我推荐无状态Handler 列表遍历的实现方式。问题四怎么优化责任链的性能参考思路可以在节点上做短路判断让某些节点在特定条件下直接跳过可以把链静态装配减少运行期初始化如果是规则特别多的重场景甚至可以引入规则引擎来替代手写链。6. 实现责任链的常见坑与我的避坑经验光会画类图还不够把责任链真的放到生产环境里跑你会遇到不少课本上压根不会提的坑。我挑了几个自己踩过或者帮别人踩过的逐个说一说。6.1 坑一调用链被某个节点悄悄打断这是初学者最容易犯的错误。写节点的时候判断完canHandle不满足条件却忘了调用successor.handleRequest或者不小心写成了直接return。代码不报错、不崩溃但请求到一个节点之后就像泥牛入海业务静默失败。排查这种问题特别折磨人。我的习惯是开发阶段在每个节点入口、出口、向下传这三个位置都打日志保证从链路日志里能清楚看到请求在哪一站被消费了。线上则打结构化日志至少要记录每个Handler的执行时间和结果。有了这些日志链路的“断裂点”一眼就能找出来。6.2 坑二链上出现循环引用导致栈溢出如果组链时不小心让某个节点的后继指向了前面的节点责任链就会变成死循环。请求在A和B之间反复横跳直到栈溢出。我见过有人设计责任链时想玩花的把链做成双向链表结果处理请求时在A和B之间来回传递无法终止。责任链的默认语义就是单向传递如果需要双向或者环状结构那已经是完全不同的模型了别硬用责任链去套。预防的办法很机械组装的时候做一次校验禁止同一个Handler实例重复出现在链上或者统一限制链的长度。如果业务上真的会出现复杂传递路径设计阶段先画一张有向无环图确保不是环。6.3 坑三滥用责任链把简单问题复杂化责任链解决的是“候选处理者多、规则频繁变化”的问题。如果你的判断逻辑只有两三个固定分支比如“数字大于0就执行A否则执行B”那直接写if-else比套责任链清楚得多。我判断要不要用责任链就三个标准处理的候选者可能多于两个吗规则是不是经常要增删希不希望以后新增处理者时不改动发送者代码三个问题里有两个或以上回答“是”再用责任链。否则请保持简单。设计模式正确与否要以是否提升了代码的可维护性为准而不是以“用没用某种模式”为准。6.4 坑四Handler有状态多线程数据串台责任链中的Handler最好设计成无状态对象。如果某个Handler内部放了一个成员变量来存临时结果多线程并发处理请求时这个变量会被多个请求互相覆盖产生各种诡异问题。我之前就见过一个真实事故某个短信发送链路上一个Handler里缓存了“当前请求的手机号”结果高并发下客户A的短信验证码发到了客户B的手机上。根本原因就是Handler有状态。正确的做法是把需要共享的数据全部放到请求对象里让Handler只做读操作和写操作不持有中间状态。如果实在不得不用有状态的Handler也要在每次请求开始前显式重置状态并且做好加锁或者ThreadLocal隔离。同时整个责任链本身在初始化之后应该是一个只读结构。如果运行时要频繁增删节点就把“修改链”当成一次重新装配操作生成一条新链而不是原地改来改去否则并发安全会让你头秃。6.5 坑五没有兜底节点整条链形同虚设我在这上面吃过大亏。当时风控链上的某个新校验器因为配置错误直接抛了异常后面的节点全被跳过线上出现了一笔不该通过的订单。事后复盘症结就在于链尾缺少一个兜底节点。后来我做责任链有两个强制要求第一每个节点处理完必须返回明确结果要么“放行”要么“拦截”第二链的末尾一定加一个兜底节点专门处理前面所有节点都不认领的请求。这个兜底节点要做真实的业务动作比如人工审核标记、返回明确错误信息而不是只打印一行日志然后放行。有了兜底节点责任链在业务上才算是闭环。6.6 坑六把责任链和其他模式搅在一起盘根错节还有最后一个坏味道就是过度组合。有些人为了展示自己的设计能力一条业务链路里同时揉进责任链、策略模式、模板方法再配合七八个泛型抽象类最后代码变成一团乱麻。我见过一个项目一个请求从Controller进来先穿两条责任链每条链上还嵌套策略结果加一个需求要同时改五个文件没人敢动。我的原则非常简单一条业务场景尽量只铺一条链。多个场景需要共用处理节点时就让同一个Handler实现多个接口而不是在运行期动态改链。如果确实需要动态拼链也一定要把拼链逻辑单独封装到配置类里业务开发不要直接接触链结构不然维护成本会高到你怀疑人生。我自己从最早觉得责任链“不过是个链表”到如今真刀真枪用它解决线上事故最大的体会就是责任链真正的价值不在链表结构本身而在于它逼着你把每个业务规则的边界想清楚。每个节点只写自己的判断逻辑彼此不互相污染新增规则就像往流水线上加一台机器不用改造整条产线。这才是这个模式最值得培养的思维方式。如果你最近正好在改一段满是if-else的审核或者过滤逻辑不妨先把链条地图画出来试试把每个判断点拆成独立节点。拆完你会回来感谢我的。
返回列表