ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

责任链模式详解:从if else到优雅的审批链实现

责任链模式详解:从if else到优雅的审批链实现 大家好我是你们的技术博主。今天我们聊一个在业务开发中出镜率奇高、但许多初学者一看到类名就发懵的设计模式——责任链模式。之前在项目里接到一个需求不同金额的采购单要走不同的审批流程请假超过 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中通过索引遍历。ListHandler 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 ListInterceptor 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 的拦截器HandlerInterceptorpreHandle返回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 version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdchain-of-responsibility-demo/artifactId version1.0-SNAPSHOT/version properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target /properties /project4.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 的 ChannelPipelineNetty 是一个高性能网络框架它的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 指向 BB 的 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 ListApprover 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逻辑重构成责任链模式感受一下变更成本的变化。如果这篇文章对你有帮助欢迎点赞、收藏、转发。你的支持是我持续输出技术干货最大的动力。
返回列表