
1. 先说场景当 if-else 开始失控前阵子我在一个订单服务里接一个新需求下单前要做参数校验、库存校验、风控校验、优惠券状态校验还要记账。一开始大家很自然地写了几个 Service 方法在 Controller 或 Facade 里挨个调用伪代码大概是public void createOrder(CreateOrderReq req) { checkParam(req); checkStock(req); checkRisk(req); checkCoupon(req); createOrderRecord(req); }看起来还行但第二个需求来了不同订单来源APP、小程序、商家后台的校验规则不一样有的来源需要额外检查该用户是否在黑名单有的来源需要跳过优惠券校验。于是代码开始变成if (APP.equals(req.getSource())) { checkRisk(req); } if (!MERCHANT.equals(req.getSource())) { checkCoupon(req); }又过了一个迭代几个同事在同一个方法里叠了十几层 if还互相不知道对方的规则。代码 review 的时候大家都在吐槽谁改都不敢动一不留神就把别人的逻辑绕过去了。这种时候我就开始考虑用责任链模式把“处理步骤”解耦成一个个独立的 Handler再让 Spring 容器来管理它们的顺序和装配。责任链模式本身并不复杂将请求沿着一条链传递每个节点决定自己是否处理、是否中断、是否继续向下传。它在 Java 里最经典的例子是 Servlet 的 FilterChainSpring MVC 里也有 Interceptor实际上底层都是责任链的变体。用 Spring Boot 来做这件事的好处是完全不用手动 new 一堆对象容器自动收集所有 Handler再通过Order控制先后顺序代码会变得非常清爽。如果你也在维护那种“一个方法里不断追加业务分支”的老项目这篇文章应该能给你一个直接能用的落地思路。下面我按自己的实现方式拆开讲。2. Spring Boot 三层核心设计Handler、Chain、Client要在 Spring Boot 里优雅地使用责任链我习惯把它拆成三层处理器抽象、链的组装、业务调用入口。这样之后无论是加节点还是调整顺序都不需要碰调用方代码。2.1 顶层抽象一个接口搞定先定义一个泛型接口适配多种请求上下文。public interface OrderHandlerT { /** * 执行当前处理器 * return true 表示继续向下传递false 表示中断链路 */ boolean handle(T context); }有的项目里会定义HandlerResult而不是 boolean比如需要区分“校验失败”“处理成功”“需要跳过下一级”这都行。但是初期我建议用 boolean简单直接链路逻辑一眼能看懂。后来需要更复杂的结果再扩展成枚举或者对象。对应的我们还需要一个用于组合处理结果的上下文对象。责任链的 Handler 之间不是零交流的前置步骤会把数据塞到 context 里后置步骤取出来用。例如Data public class OrderContext { private Long userId; private String source; // 订单来源 private Long skuId; private Integer num; private BigDecimal amount; private Boolean blacklistChecked; private Boolean couponValid; }这里的OrderContext就是整条链上共享的“传递单”。使用 Lombok 也好手写 getter/setter 也好重点是把每个 Handler 关注的数据拆开避免在 Handler 之间传一大堆参数。2.2 责任链执行器与链条组装接着定义一个执行器组件负责从 Spring 容器中拿出所有OrderHandler按顺序排好然后遍历执行Component public class OrderHandlerChainT { private final ListOrderHandlerT handlers; public OrderHandlerChain(ListOrderHandlerT handlers) { this.handlers handlers; } public boolean execute(T context) { for (OrderHandlerT handler : handlers) { boolean continueFlag handler.handle(context); if (!continueFlag) { // 这里可以记录日志哪个处理器中断了 return false; } } return true; } }关键点在于构造函数里的ListOrderHandlerT handlers。Spring 看到一个接口类型的集合会自动把容器里所有该接口的实现类都注入进来而且会使用 Spring 内置的排序比较器AnnotationAwareOrderComparator来处理Order注解和Ordered接口。也就是说排序是容器级别保证的不是我们自己Collections.sort写的。这里的类型 T 也可以直接固定成OrderContext不搞泛型。我一般用泛型是因为项目里还有另外一条“售后处理链”、一条“消息通知链”复用同一个执行器代码非常方便。2.3 业务侧调用方式调用的时候非常简单把上下文装好直接执行链Service public class OrderService { private final OrderHandlerChainOrderContext orderHandlerChain; public OrderService(OrderHandlerChainOrderContext orderHandlerChain) { this.orderHandlerChain orderHandlerChain; } public void createOrder(CreateOrderReq req) { OrderContext context new OrderContext(); // 把 req 转成 context context.setUserId(req.getUserId()); context.setSource(req.getSource()); context.setSkuId(req.getSkuId()); context.setNum(req.getNum()); context.setAmount(req.getAmount()); boolean pass orderHandlerChain.execute(context); if (!pass) { throw new BizException(订单创建失败前置校验未通过); } // 继续执行后续业务…… } }这样业务入口只关心“链是否通过”不关心链上有多少节点、每个节点是什么逻辑。后续加一个xxxHandler实现类链上自动多一级去掉一个 Handler只需要删掉类或者在上面加Component的排除条件调用方代码一行不用改。3. Order 到底在控制什么怎么用好它责任链的核心是顺序。Spring 里控制顺序最常见的三种方式Order注解、实现Ordered接口、实现PriorityOrdered接口。我们的链路会大量使用Order所以这里有必要把它彻底说明白。3.1 Order 注解与 Ordered 接口Order可以加在类、方法、字段上。在 Spring Boot 的很多地方它都会被读取最典型的就是 AOP 切面排序、事件监听器排序、命令模式排序。对于集合注入Spring 会把Order的值解析出来值越小优先级越高越靠前。比如这样两个 HandlerComponent Order(100) public class StockCheckHandler implements OrderHandlerOrderContext { ... } Component Order(10) public class ParamCheckHandler implements OrderHandlerOrderContext { ... }Spring 注入的ListOrderHandlerOrderContext中ParamCheckHandler会排在StockCheckHandler前面因为 10 100。如果是相同值顺序由 Spring 底层的OrderComparator决定但不保证稳定所以业务上不要依赖同优先级顺序。我个人习惯把Order的数值按十进制段来规划比如0 ~ 99纯参数校验100 ~ 199库存、金额等核心数据校验200 ~ 299风控、黑白名单等外部规则300 ~ 399优惠、营销类处理400兜底、日志、埋点这样在多个 Handler 之间插入新节点时只需要选中合适的数段即可不容易因为顺序改来改去导致冲突。3.2 同优先级怎么办实现时的隐藏细节如果确实需要多个 Handler 在同一优先级内部保持某个逻辑顺序我建议不要完全交给容器。你可以在执行器里自定义一个“二次排序”比如利用上下文里的某个标记动态排序。或者更简单在 Handler 里自己判断是否继续传递比如将Order(100)的 Handler 内部再分成 if 分支把紧密相关的两段逻辑写在一起。另一个隐藏细节Order的默认值。Spring 的Order注解定义是int value() default Ordered.LOWEST_PRECEDENCE;而Ordered.LOWEST_PRECEDENCE Integer.MAX_VALUE。这意味着如果你给某个 Handler 只加了Component但没有写Order它的排序值默认是2147483647基本就是最后执行。很多人说“我的 Handler 没排序结果跑在最后一个”原因就在这里。反过来Ordered.HIGHEST_PRECEDENCE是Integer.MIN_VALUE也就是最前面。如果要定义一个最优先的节点可以写Order(Ordered.HIGHEST_PRECEDENCE)但一般不建议用极值留点空间给后面的扩展更舒服。4. 完整实战订单校验责任链空讲概念不够我把我实际用的一套订单校验链路完整贴出来。这个链路不复杂但足够说明问题参数校验、库存校验、风控校验、优惠券校验四个节点。4.1 Handler 接口与执行器定义这里我将上面的代码补全成能跑的样子。先定义 Handler 接口public interface OrderHandler { /** * 处理订单上下文 * param context 订单上下文 * return true 继续链false 中断链 */ boolean handle(OrderContext context); }执行器依然是泛型版但为了简洁也可以固定类型Component public class OrderHandlerChain { private final ListOrderHandler handlers; public OrderHandlerChain(ListOrderHandler handlers) { this.handlers handlers; } public boolean execute(OrderContext context) { for (OrderHandler handler : handlers) { boolean goOn handler.handle(context); log.info(执行处理器: {}, 结果: {}, handler.getClass().getSimpleName(), goOn); if (!goOn) { return false; } } return true; } }实际项目里我还会加一个短路后的异常类型存储到 context 中这样抛出业务异常时可以携带具体失败原因。比如 context 里放一个String errorMsg中断时设置错误信息然后由调用方读取并抛出而不是在 Handler 里直接 throw。这样链的可测试性会好很多。不过新手容易踩坑如果 Handler 里直接 throw 了后面的链根本不会执行也不是不行但要意识到这是“强中断”与返回 false 的“弱中断”语义不同。4.2 四个具体 Handler 实现第一个参数校验Component Order(10) public class ParamCheckHandler implements OrderHandler { Override public boolean handle(OrderContext context) { if (context.getUserId() null) { context.setErrorMsg(用户ID不能为空); return false; } if (context.getSkuId() null) { context.setErrorMsg(商品ID不能为空); return false; } if (context.getNum() null || context.getNum() 0) { context.setErrorMsg(购买数量必须大于0); return false; } return true; } }第二个库存校验。这里的库存我直接从某个接口查出来实际项目可能是调用库存中心或查本地库存表Component Order(20) public class StockCheckHandler implements OrderHandler { private final StockService stockService; public StockCheckHandler(StockService stockService) { this.stockService stockService; } Override public boolean handle(OrderContext context) { Integer stock stockService.getStock(context.getSkuId()); if (stock null || stock context.getNum()) { context.setErrorMsg(库存不足); return false; } // 可以把锁定库存数量放到上下文里后面的 Handler 可能用到 context.setStock(stock); return true; } }第三个风控校验。这个 Handler 专门演示不同来源的差异化规则Component Order(30) public class RiskCheckHandler implements OrderHandler { private final RiskService riskService; public RiskCheckHandler(RiskService riskService) { this.riskService riskService; } Override public boolean handle(OrderContext context) { // APP 来源走强风控 if (APP.equals(context.getSource())) { boolean safe riskService.checkBlacklist(context.getUserId()); if (!safe) { context.setErrorMsg(当前用户存在风险请联系客服); return false; } context.setBlacklistChecked(true); return true; } // 其他来源先放开后续可以再收紧 context.setBlacklistChecked(false); return true; } }第四个优惠券校验Component Order(40) public class CouponCheckHandler implements OrderHandler { private final CouponService couponService; public CouponCheckHandler(CouponService couponService) { this.couponService couponService; } Override public boolean handle(OrderContext context) { // 商家后台下单不支持优惠券跳过 if (MERCHANT.equals(context.getSource())) { context.setCouponValid(false); return true; } boolean valid couponService.checkUserCoupon(context.getUserId(), context.getCouponId()); if (!valid) { context.setErrorMsg(优惠券不可用); return false; } context.setCouponValid(true); return true; } }这四个 Handler 写完加入 Spring 容器后责任链就自动组建完成。ParamCheckHandler先跑过了再跑库存库存过了再跑风控最后跑优惠券。如果哪个校验失败后续就不再执行。4.3 运行结果与执行顺序我写了一个很小的测试入口SpringBootTest class OrderHandlerChainTest { Autowired private OrderHandlerChain orderHandlerChain; Test void testChain() { OrderContext ctx new OrderContext(); ctx.setUserId(123L); ctx.setSource(APP); ctx.setSkuId(999L); ctx.setNum(2); ctx.setAmount(new BigDecimal(199.00)); boolean pass orderHandlerChain.execute(ctx); System.out.println(链执行结果: pass); } }如果所有数据都正确日志输出顺序是这样的执行处理器: ParamCheckHandler, 结果: true 执行处理器: StockCheckHandler, 结果: true 执行处理器: RiskCheckHandler, 结果: true 执行处理器: CouponCheckHandler, 结果: true 链执行结果: true如果把库存数量改成超出库存的 999输出会变成执行处理器: ParamCheckHandler, 结果: true 执行处理器: StockCheckHandler, 结果: false 链执行结果: false后面的风控和优惠券不会执行这就是典型的短路逻辑。很多业务场景里我们不需要“所有节点都处理完”再做决定只要有一个前置条件不满足就立即终止这正是责任链模式比逐个调用更优雅的地方。5. 责任链与策略模式的区别选型不纠结写责任链的时候很多人会问这跟策略模式有什么区别我什么时候该用策略什么时候该用责任链其实两者可以解决类似问题但关注点不同。策略模式核心是“算法可替换”调用方在运行时选择一个具体的策略来执行。通常几个策略是互斥的只会选择其中一个。比如支付方式支付宝、微信、余额三选一。策略模式更强调Context持有策略引用调用方自己决定策略。责任链模式核心是“请求在链上流动”每个节点依次判断可以多个节点参与也可以在中途中断。它更强调“职责的分离”和“顺序的编排”。比如订单校验、审批流、消息过滤这类场景天然存在多个依次执行的环节每个环节负责不同职责。我自己的选型经验是如果业务是“多选一”优先策略模式如果业务是“有序地一个接一个处理”优先责任链。实际项目里还会遇到两者结合先用策略模式挑选出一条链路再在这条链路上组合多个 Handler这样整体设计会更有弹性。还有一点值得提责任链模式不一定非要用容器注入列表。传统责任链书籍里会用链式 setNext 的方式手动串联但那样太不够“Spring”。我们使用ListHandler自动注入后新增节点不需要改动链的结构代码这是我觉得最实用的一点。6. 常见问题与排查技巧实录这部分是干活时最头疼的地方。责任链模式写起来爽出问题了也容易懵。我整理了实际碰到过的几个典型问题以及排查思路。6.1 Handler 没有被注入链上没执行如果你发现链里只有一个 Handler或者根本一个都没有先检查两点Handler 类是否加了Component或对应注解Service也行但要注意语义。是否在SpringBootTest或某个模块扫描路径之外。Spring Boot 默认扫描启动类所在包及子包如果你的 Handler 放在别的包且没加ComponentScan它根本不会被容器发现。检查手段也很简单在执行器的构造函数里打日志打印handlers.size()和每个 handler 的 class 名称。或者临时在ApplicationRunner里注如ListOrderHandler打印。如果 list 为空说明 Bean 没被扫描到。6.2 顺序乱跑Order 失效的原因最让人困惑的就是明明写了Order(10)结果被Order(20)的 handler 先执行。常见原因有Handler 实现类没有直接实现OrderHandler接口而是继承了某个父类父类实现了接口。此时Order注解是否被 Spring 识别取决于注解标记的位置。Spring 对Order的处理会检查类本身也会检查接口和父类上的Order。如果你把Order标记在父类上那么所有子类可能有同样的顺序导致“看起来没生效”。使用了 CGLIB 代理。某些情况下 Handler 被代理后Order的注解信息还可以通过 AnnotationUtils 取得一般没问题。但如果你自己手动 new 了一个 Handler 并加入 list没有交给 Spring 排序那执行器里的 list 顺序就只是 ArrayList 添加顺序。多个 Handler 的Order值相等。Spring 对同值不保证先后需要重新设计数值。一个排查技巧在OrderHandlerChain构造方法里直接打印排序后的 handler 名称一眼看出顺序和你预期差在哪。6.3 状态污染千万别在 Handler 里存业务数据责任链的 Handler 默认是单例 Bean多个请求会共享同一个实例。如果你在 Handler 里写了一个成员变量来暂存数据比如Component Order(10) public class RiskCheckHandler implements OrderHandler { private boolean pass; Override public boolean handle(OrderContext context) { this.pass false; // ... return this.pass; } }一旦有并发请求两个线程同时修改同一个pass字段数据就串了。责任链里所有临时结果都应该放在OrderContext中传递Handler 自身要保持无状态。这是我踩过最坑的一个点线上偶发误判会让你查到头秃。6.4 循环依赖Handler 之间别互相注入有时候你会觉得 Handler B 需要调用 Handler A 的逻辑直接把 A 注入进来。容易搞出循环依赖A 注入 BB 注入 A。Spring Boot 2.6 之后默认不允许循环依赖了启动直接报错。解决思路有三个把重复逻辑抽取到单独的 Service两个 Handler 都注入 Service。把 Handler A 要暴露的结果放进 contextHandler B 直接读 context不调用 A。如果确实是链路问题就调整 Handler 的职责边界让每个 Handler 只做自己的事。我通常首选第二种这也能让 Handler 之间的依赖彻底消失。6.5 动态链路按业务类型自由组合一个项目里不可能只有一条链。比如下单校验、售后审核、消息通知各自需要不同的 Handler 集合。如果都用同一个泛型接口注入 List 时会全部混在一起。解决办法是给 Handler 加业务类型分组。比如定义注解Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) public interface ChainType { String value(); }每个 Handler 类上加ChainType(order)或者ChainType(afterSale)然后在执行器里从所有 Handlers 中筛选当前需要的类型Component public class ChainFactory { private final ListOrderHandler handlers; public ChainFactory(ListOrderHandler handlers) { this.handlers handlers; } public ListOrderHandler getHandlers(String type) { return handlers.stream() .filter(h - h.getClass().isAnnotationPresent(ChainType.class)) .filter(h - type.equals(h.getClass().getAnnotation(ChainType.class).value())) .sorted(AnnotationAwareOrderComparator.INSTANCE) .collect(Collectors.toList()); } }不过这种方法会把所有 Handler 一次性注入进来Handler 数量多了也不大。更优雅一点的做法是“分容器注入”比如定义多个接口public interface OrderChainHandler extends OrderHandler {} public interface AfterSaleChainHandler extends OrderHandler {}Spring 会分别注入ListOrderChainHandler和ListAfterSaleChainHandler互不干扰。每个业务链用执行器也有对应的泛型代码会非常清晰。实际项目中我偏向这种接口分层法。7. 写在最后的心得责任链模式本身不是什么高深的技术但在 Spring Boot 项目里用好了能极大降低后续加需求带来的心理压力。我最深的体会是当一段业务被拆成多个 Handler 后每个 Handler 都非常小测试用例好写review 也好过。你不需要在一大段 if-else 里去猜某段逻辑的边界在哪里。如果这篇文章你觉得还是有距离我建议你从最简单的三个节点开始参数校验、权限校验、业务校验。先把链搭起来再慢慢加节点。用Order控制顺序把中间结果放到 Context 里坚持“Handler 无状态”的原则。等跑通一个场景后你会自然发现日志也清晰了改规则也敢动了。最后分享一个小技巧在责任链的 execute 方法里我习惯把每个 Handler 的执行结果、执行耗时都记录到日志或埋点系统而不是只记录最终结果。因为链路里一旦某个节点出问题你能立刻看到是哪个 Handler 花了多长时间、在哪个环节断掉。这种可观测性在实际排查线上问题时比任何设计上的“优雅”都更救命。