Java责任链模式实战:从审批流到过滤器链的设计模式指南
面试里聊“设计模式”责任链模式是出现频率很高的一个。很多同学看到Handler、Filter、Interceptor这类词就觉得难其实拆开看一点都不复杂它就是把一堆“能处理某件事的对象”串成一条链请求从链头进来一个个往下传谁有资格处理、谁来处理、处理完要不要继续传给下一个都由链上的节点自己决定。这篇内容会从“责任链模式是什么”开始讲然后用 Java 代码把请假审批、敏感词过滤这类常见场景写出来再对比装饰器、策略、观察者这些容易混的模式最后补一套面试问答和排查清单。无论是准备“设计模式期末”考试、做“设计模式大作业”还是想在企业项目里用 Java 正确落地责任链模式都可以直接对着本文操作。1. 责任链模式核心能力速览维度说明模式类型行为型设计模式核心思想把请求发送者和请求处理者解耦让多个处理者依次尝试处理请求参与角色抽象处理者Handler、具体处理者ConcreteHandler、客户端Client关键操作设置下一个处理者 next每个处理者决定自己处理还是继续传递核心价值动态组合处理流程、避免 if-else 堆积、方便新增和调整处理节点终止方式两种任一处理者处理后终止所有处理者按管道方式逐级执行常见实现单向链表方式setNext、List 合方式集合保存 Handler 列表典型应用审批流、过滤器链、拦截器、日志级别处理、风控规则链、异常处理链学习难度中低重点在于理解“传递”和“终止”两个动作适合读者Java 后端开发、正在准备设计模式面试/期末/大作业的读者从使用者的角度看责任链模式最值得关注的一点是它把一段“连续的 if-else 判断”变成了可扩展的节点集合。以后新增规则、调整规则顺序都不需要改业务主流程只要重新组装链就行。2. 责任链模式解决什么问题2.1 耦合问题假设你现在写了一个下单接口里面需要做参数校验、登录状态校验、用户权限校验、风控校验。如果全写在一个方法里代码会长成下面这样public void createOrder(OrderRequest request) { if (request null) { throw new IllegalArgumentException(请求不能为空); } if (request.getUserId() null) { throw new IllegalStateException(用户未登录); } if (!userService.hasPermission(request.getUserId())) { throw new IllegalStateException(无权限); } if (!riskService.pass(request)) { throw new IllegalStateException(风控不通过); } orderService.create(request); }这个写法不是不能用问题是校验逻辑和业务逻辑耦合在一起订单方法越来越长。今天要加一个“黑名单校验”明天要加一个“库存校验”主流程方法一直在改。如果不同渠道的下单流程不一样比如普通用户走全部校验、会员跳过部分校验你就只能复制一大堆 if-else。责任链模式的思路很直接把每个校验抽出来做成独立的处理者。下单请求进来先走到参数校验节点再走到登录校验节点再走到权限校验节点。每个节点只负责判断自己能不能处理不能处理或处理后需要放行就传给下一个节点。主流程完全不需要知道链内部有多少个节点。2.2 扩展问题用责任链模式之后新增一个校验节点只需要新建一个类然后在组装链的地方把它挂到后面业务代码一行都不用改。这就是“开闭原则”的落地对扩展开放对修改关闭。2.3 顺序问题业务规则往往有顺序要求。比如先判断用户是否存在再判断是否有权限先做基础校验再做风控判断。责任链天然支持这种顺序控制因为链的传递方向是固定的组装顺序就是执行顺序。3. 责任链模式的适用场景责任链模式不是万能药用得最多的是下面几类场景。3.1 审批流请假、报销、采购审批是责任链模式最经典的例子。不同金额、不同天数对应不同的审批人请假 3 天以内主管审批即可。请假 3 到 7 天需要部门经理审批。请假超过 7 天需要总监甚至更高级别审批。这类场景有几个特点处理者是分层级的请求会依据条件落在某一个层级一旦有人处理完请求就不需要继续往下传。3.2 过滤器链Java Web 开发中的Filter、Spring MVC 中的HandlerInterceptor本质上都是责任链思想。请求经过一串过滤器每个过滤器都可以决定放行还是拦截。和审批流不同的是过滤器链通常每个节点都会执行节点之间是“管道式”关系。3.3 日志级别处理java.util.logging.Logger就是这么设计的日志记录器可以把消息传给父级记录器每一级都可以决定要不要记录、要不要继续向上传递。3.4 风控规则链支付风控里经常用到责任链。一个支付请求过来要依次检查设备指纹、账户历史行为、金额异常、黑名单。每个规则节点都有机会终止这个请求也可以放行让下一个节点继续检查。3.5 异常处理链异常处理也可以做成链式结构。捕获到一个异常后先尝试用 A 处理器转化处理不了传给 B 处理器再不行传给兜底处理器。3.6 消息中间件管道Netty 的Pipeline、OKHttp 的拦截器链都是责任链模式在框架层面的实现。请求进入管道后经过一个个 handler每个 handler 都可以决定继续传递还是短路返回。4. 不应该使用责任链模式的场景责任链模式虽然灵活但也不是所有场景都合适。处理逻辑非常简单且固定不变。如果就是一个两层的 if-else强行抽象成责任链只会增加代码量。性能要求极高、链节很多。每经过一个节点都是一次对象引用和方法调用链太长时会有额外开销。节点之间职责严重重叠。多个处理者都能处理同一类请求顺序稍微一变结果就不同排查问题会很难受。团队成员不熟悉该模式。设计模式一旦用不好就会变成“过度设计”。如果团队对责任链不熟建议先在小范围试点保留必要的注释。5. Java 实现经典表驱动写法 vs 手写链表责任链模式在 Java 里有两种典型写法一种是经典的单向链表结构每个处理器持有下一个处理器的引用另一种是使用集合统一管理处理器列表。两种都能解决实际问题下面分别给出代码。我们先演示最经典的写法。5.1 抽象处理者/** * 请假审批抽象处理者 */ public abstract class Approver { /** * 下一个审批节点 */ protected Approver next; /** * 设置下一级审批人 */ public void setNext(Approver next) { this.next next; } /** * 处理请假请求 * * param days 请假天数 */ public abstract void approve(int days); }这里最关键的是protected Approver next字段。它让抽象处理者天然具备“指向下一个节点”的能力。子类继承之后既可以使用next继续传递也可以直接结束链。5.2 具体处理者具体处理者要回答两个问题当前请求我能不能处理如果不能处理要不要传给next请假 3 天以内的场景代码可以这样写/** * 主管3天以内可以审批 */ public class LeaderApprover extends Approver { Override public void approve(int days) { if (days 3) { System.out.println(主管审批通过 days 天); } else if (next ! null) { next.approve(days); } } }/** * 部门经理7天以内可以审批 */ public class ManagerApprover extends Approver { Override public void approve(int days) { if (days 7) { System.out.println(部门经理审批通过 days 天); } else if (next ! null) { next.approve(days); } } }/** * 总监没有明确上限但不能超过15天 */ public class DirectorApprover extends Approver { Override public void approve(int days) { if (days 15) { System.out.println(总监审批通过 days 天); } else { System.out.println(超过 15 天需要线下复核); } } }这里的终止条件有两种表现LeaderApprover和ManagerApprover中当条件不满足时会调用next.approve(days)这是“传递”。DirectorApprover作为链尾没有next代表链路到这里结束。5.3 客户端组装链责任链模式的使用者不需要知道每个节点的内部实现只需要负责“建链”public class Client { public static void main(String[] args) { Approver leader new LeaderApprover(); Approver manager new ManagerApprover(); Approver director new DirectorApprover(); leader.setNext(manager); manager.setNext(director); System.out.println(请假 2 天); leader.approve(2); System.out.println(请假 5 天); leader.approve(5); System.out.println(请假 10 天); leader.approve(10); System.out.println(请假 20 天); leader.approve(20); } }运行结果如下请假 2 天 主管审批通过2 天 请假 5 天 部门经理审批通过5 天 请假 10 天 总监审批通过10 天 请假 20 天 超过 15 天需要线下复核从运行结果可以清楚看到请求永远从链头leader进入但落在哪一层处理由每个节点自己决定。客户端只调用了leader.approve(...)完全感知不到链的存在。5.4 关于“空指针”和链路断裂上面的代码里如果某个处理者不满足条件也没有设置next请求就会无响应。实际开发中更稳妥的做法是增加一个兜底节点比如/** * 兜底处理者保证链路不会无结果 */ public class DefaultApprover extends Approver { Override public void approve(int days) { System.out.println(默认处理转人工审批); } }然后把链尾指向兜底节点。这就是责任链模式的两面性灵活性高但链的完整性依赖装配代码。后面第 9 章的排查清单里会专门讲链路断裂问题。6. 更工程化的实现List 方式 责任链模式变体手写setNext适合教学和简单场景真实项目中我更推荐用 List 来管理处理器。原因有三个责任链的顺序更容易维护排序逻辑可以独立出来。支持 Spring 自动注入新增处理器不用反复修改setNext。可以统一实现“过滤器管道”式的变体每个节点都执行。下面给出一个更接近生产环境的示例。6.1 定义过滤器和过滤器链接口以文本敏感词过滤为例先定义统一的过滤器接口public interface TextFilter { /** * 过滤内容 * * param content 原始内容 * return 过滤后的内容 */ String doFilter(String content); }6.2 实现多个过滤器节点广告词过滤public class AdFilter implements TextFilter { Override public String doFilter(String content) { return content.replace(加微信, **); } }违规词过滤public class IllegalFilter implements TextFilter { Override public String doFilter(String content) { return content.replace(违禁词, **); } }重复词清洗public class RepeatFilter implements TextFilter { Override public String doFilter(String content) { return content.replaceAll(([a-z])\\1{2,}, $1$1); } }6.3 由链统一执行import java.util.List; public class TextFilterChain { private final ListTextFilter filters; public TextFilterChain(ListTextFilter filters) { this.filters filters; } public String filter(String content) { String result content; for (TextFilter filter : filters) { result filter.doFilter(result); } return result; } }6.4 客户端使用import java.util.List; public class FilterClient { public static void main(String[] args) { TextFilterChain chain new TextFilterChain( List.of(new AdFilter(), new IllegalFilter(), new RepeatFilter()) ); String input 你好欢迎加微信这条消息包含违禁词; String output chain.filter(input); System.out.println(原始内容 input); System.out.println(过滤结果 output); } }这与经典责任链模式有一个关键差异经典责任链可能“命中即终止”而过滤器链通常“依次全部执行”。从模式定义上看过滤器链是责任链的一种变体很多教材里也叫“纯责任链模式”和“不纯责任链模式”类型特点典型例子纯责任链模式请求要么被某个节点处理要么到达链尾结束请假审批不纯责任链模式每个节点都参与处理处理后继续向下传递Servlet Filter、敏感词过滤管道理解这一点很重要面试或者期末考题经常会问“责任链模式的请求一定会被处理吗”这类问题答案就是不一定取决于你用哪种变体。7. 实战案例用责任链模式重构登录校验把理论落到真实业务上我们再看一个能直接用于“设计模式大作业”的案例登录校验链。假设登录接口要求先校验参数再校验验证码再校验账号状态最后记录日志。用责任链模式重构之后主流程会很干净。7.1 定义请求封装类和处理器接口public class LoginRequest { private String username; private String password; private String captcha; public LoginRequest(String username, String password, String captcha) { this.username username; this.password password; this.captcha captcha; } public String getUsername() { return username; } public String getPassword() { return password; } public String getCaptcha() { return captcha; } }/** * 登录校验处理器 */ public interface LoginHandler { /** * param request 登录请求 * return 是否校验通过通过返回 true失败返回 false */ boolean check(LoginRequest request); }7.2 编写具体校验节点参数校验public class ParameterValidHandler implements LoginHandler { Override public boolean check(LoginRequest request) { if (request null || request.getUsername() null || request.getUsername().isEmpty() || request.getPassword() null || request.getPassword().isEmpty()) { System.out.println(参数校验失败用户名或密码不能为空); return false; } System.out.println(参数校验通过); return true; } }验证码校验public class CaptchaValidHandler implements LoginHandler { Override public boolean check(LoginRequest request) { if (request.getCaptcha() null 1234.equals(request.getCaptcha())) { System.out.println(验证码校验失败); return false; } System.out.println(验证码校验通过); return true; } }这里故意留了一个逻辑让大家思考真实业务里验证码肯定是用户输入的null时应该怎么处理需要对业务场景做取舍。示例中使用了 null 是从代码可读性角度演示“条件判断”实际项目请根据接口设计补充完整逻辑。账号状态校验import java.util.Set; public class AccountStatusHandler implements LoginHandler { private static final SetString BLACK_LIST Set.of(forbidden-user); Override public boolean check(LoginRequest request) { if (BLACK_LIST.contains(request.getUsername())) { System.out.println(账号状态校验失败账号已被封禁); return false; } System.out.println(账号状态校验通过); return true; } }7.3 用责任链串起来执行import java.util.List; public class LoginHandlerChain { private final ListLoginHandler handlers; public LoginHandlerChain(ListLoginHandler handlers) { this.handlers handlers; } public boolean doCheck(LoginRequest request) { for (LoginHandler handler : handlers) { if (!handler.check(request)) { return false; } } return true; } }7.4 测试效果import java.util.List; public class LoginServiceDemo { public static void main(String[] args) { LoginHandlerChain chain new LoginHandlerChain( List.of( new ParameterValidHandler(), new CaptchaValidHandler(), new AccountStatusHandler() ) ); LoginRequest okRequest new LoginRequest(zhangsan, 123456, 1234); LoginRequest blackRequest new LoginRequest(forbidden-user, 123456, 1234); System.out.println(第一次登录 chain.doCheck(okRequest)); System.out.println(---); System.out.println(第二次登录 chain.doCheck(blackRequest)); } }这个例子和手写setNext有本质区别校验链是“短路式”的只要有一个节点返回false整条链直接终止。它保留了责任链“多个处理者依次尝试”的核心结构同时又比单向链表更好维护。真实项目里用 Spring 时可以这样接管Service public class LoginValidateService { private final LoginHandlerChain chain; public LoginValidateService(ListLoginHandler handlerList) { this.chain new LoginHandlerChain(handlerList); } public boolean validate(LoginRequest request) { return chain.doCheck(request); } }Spring 启动时会把容器中所有LoginHandler类型的 Bean 按顺序注入到handlerList里。如果你需要控制顺序可以通过Order注解或者在LoginHandler上增加排序方法。Order示例import org.springframework.core.annotation.Order; Order(1) public class ParameterValidHandler implements LoginHandler { // ... }不过要注意Order注解需要配合 Spring 的AnnotationAwareOrderComparator才有意义。如果直接手动构造List.of(...)顺序还是以你写的参数顺序为准。8. 责任链模式与相关设计模式对比责任链模式经常和装饰器模式、策略模式、观察者模式混在一起考。这里整理一个对比表。对比点责任链模式装饰器模式策略模式观察者模式类型行为型结构型行为型行为型核心意图多个处理者依次尝试处理请求动态扩展核心对象功能封装可替换的算法一对多通知依赖更新执行方式串行传递可能中途停止层层包装逐个增强一次选择并执行一个算法广播给多个观察者请求去向从链头传到链尾或命中即停从外层传到内层核心调用者选定一个策略对象状态变化通知所有观察者是否扩展处理者容易新增节点不影响主流程容易新增装饰器不影响核心容易新增策略实现即可容易新增观察者即可典型场景审批、过滤器、拦截器IO 流缓冲、加解密包装支付渠道切换、排序算法事件监听、消息订阅8.1 责任链和装饰器的区别这是面试最高频的对比题目。核心区别在于“有没有终止语义”和“谁在调用谁”。责任链中请求是“横向流动”的A 处理完可以传给 BB 处理完可以传给 C也可以选择不传。装饰器中包装关系是“纵向嵌套”的外层装饰器调用内层对象整个调用链一定会到达最核心的被包装对象。简单记法责任链是“队友之间传递请求”装饰器是“一层层包洋葱”。8.2 责任链和策略的区别策略模式的核心是“选择一种算法”责任链的核心是“多个对象轮流尝试处理”。策略模式是客户端主动选责任链是客户端不知道谁会处理只负责发请求。8.3 责任链和观察者的区别观察者是广播一个状态变化所有观察者都会收到通知。责任链是接力请求从链头进入链中的节点决定是否继续向下传。看到“广播”就是观察者看到“接力”就是责任链。9. 责任链模式面试高频问题与答题框架这里给出几道常见的责任链模式问题并给出答题要点。9.1 责任链模式和装饰器模式区别是什么答题框架先讲意图再讲结构最后给例子。责任链模式是行为型模式目的是解耦请求者和处理者让多个处理者依次尝试处理请求。装饰器模式是结构型模式目的是动态增强对象功能。责任链的请求可能在中间节点结束装饰器的调用链通常一定会走到核心对象。例子审批流是责任链IO 缓冲流是装饰器。9.2 责任链模式中如何处理顺序问题经典写法通过setNext方法控制顺序谁后设置谁就排在后面。List 方式可以通过Order排序或者显式指定 List 顺序。设计上应该把“顺序敏感”的节点放在前面把“兜底节点”放在最后。9.3 责任链模式会不会出现死循环会。如果某个节点的next指向了自己或者链中出现环请求就会无限循环。解决办法装配链时避免循环引用。增加最大传递次数限制。在框架中维护一个已访问节点集合发现重复节点直接抛出异常。9.4 责任链模式会不会影响性能会。每个节点都是一次对象调用和判断链路越长开销越大。在性能敏感场景要控制链长度或者用日志统计每个节点的耗时。9.5 JDK/Servlet/Spring 中哪里用到了责任链java.util.logging.Logger日志处理器链。Servlet 的FilterChain过滤器链。Spring MVC 的HandlerInterceptor拦截器链。Netty 的ChannelPipeline管道处理器链。MyBatis 的Interceptor插件拦截链。10. 责任链模式常见问题与排查方法责任链模式代码量不大但排查问题时如果有以下几个常见毛病会很头疼。问题现象可能原因排查方式解决方案请求到某个节点后没有结果该节点不满足条件next为 null检查链尾是否有兜底处理者增加DefaultNode兜底链路执行顺序不对setNext顺序装配错误或 List 排序未生效打印整个链的节点顺序统一用 List 管理并显式排序请求被连续处理多次节点内误调用了next导致本应终止却继续传递检查每个节点是否多余调用next明确“终止”和“传递”逻辑出现死循环链存在环引用增加访问次数限制或断点观察装配链时避免循环引用新增节点总被漏掉手工写setNext时没有把新节点挂到链尾检查装配代码自动注入 List 管理处理器调试时不知道请求到哪个节点没有链路日志在每个节点输出日志或使用 traceId增加统一日志切面10.1 链路空转问题责任链中如果每个节点都不能处理该请求链会一直传到链尾。链尾必须承担兜底职责否则请求就是“静默失败”。。这句结尾应该是“链条断掉”我补正一下链尾必须承担兜底职责否则请求就是“静默失败”这对线上问题排查是致命的。建议在链尾放一个DefaultHandler负责记录日志并返回统一结果。10.2 异常处理问题每个节点都可能抛异常。最简单的处理方式是在链的执行入口统一捕获try { chain.doCheck(request); } catch (Exception e) { log.error(责任链执行异常节点{}, currentHandlerName(), e); // 根据业务决定是降级还是终止 }不要把异常处理散落在每个节点里除非某个节点需要把异常转化为特定返回结果。11. 责任链模式最佳实践与代码设计建议11.1 每个节点只做一件事责任链最大的优势是“单点职责”。如果某个节点既做参数校验又做数据清洗还写日志那它就把链污染了。节点之间应尽量独立最好只依赖请求对象。11.2 用 List 代替手动 setNext除非是教学或考试要求否则不推荐使用经典setNext写法。List 方式更容易维护顺序、更容易进行测试也更符合 Spring 项目的习惯。11.3 明确“终止”还是“继续”每个节点在实现时必须先想清楚处理成功后要不要传给下一个节点处理失败时是直接抛出异常还是把错误信息放进请求对象然后继续不要让节点在“继续执行”和“终止链路”之间反复横跳。一个稳定团队应该约定统一范式校验类链失败即返回成功继续。过滤类链每个节点都执行最终返回处理结果。审批类链命中即终止未命中继续。11.4 增加链路日志和 traceId责任链一旦变长定位问题就难了。在入口生成一个traceId每个节点打印“进入-离开”日志会大幅降低排查成本。public class TraceContext { private static final ThreadLocalString TRACE_ID new ThreadLocal(); public static void set(String traceId) { TRACE_ID.set(traceId); } public static String get() { return TRACE_ID.get(); } public static void clear() { TRACE_ID.remove(); } }每个节点可以在日志中输出2025-06-01 10:00:00.123 [traceIdabc] ParameterValidHandler start 2025-06-01 10:00:00.124 [traceIdabc] ParameterValidHandler success11.5 定义统一的返回结果节点之间如果只是传递原始请求每个节点都把结果写到 request 的字段里容易出现“谁修改了什么”的混乱。建议在请求对象中增加一个结果容器或者让链执行层统一处理返回结果。11.6 注意发布顺序和灰度责任链的顺序一旦变更可能会影响线上行为。比如把风控节点从链尾移到链头等于把所有请求都先做了风控判断。发布前要在测试环境把每个节点的命中日志跑一遍确认顺序符合预期。12. 总结与下一步责任链模式的核心价值就是一句话把“判断谁来处理”的逻辑从业务主流程中拆出来交给一条可以动态调整的链。如果你正准备“设计模式期末”考试或“设计模式大作业”建议按这个顺序练习先手写一遍setNext审批链再用 List 方式实现一个登录校验链最后把责任链和装饰器、策略的对比整理成表格。做“设计模式 java 实现”相关作业时可以选取某个具体业务场景比如订单风控、文本审核用责任链模式写出可运行的代码比单纯背概念更容易拿高分。如果你已经有工作经验接下来值得做两件事检查现有代码里有没有“连续 if-else 多个判断”的代码块尝试用责任链模式重构。阅读一下所使用框架中的责任链实现比如 Dubbo 的Filter链、Spring MVC 的拦截器链分析它们内部是怎么组织节点顺序和处理终止逻辑的。责任链模式本身不难难点在于判断“什么时候该用、什么时候不该用”。从最小的两个节点开始理解它的传递、终止和兜底三个机制后续看框架源码会轻松很多。