责任链模式:从 if else 到 Spring 自动装配

责任链模式:从 if else 到 Spring 自动装配

一、责任链模式是什么?

责任链模式,英文叫 Chain of Responsibility Pattern。

它的核心思想是:

一个请求来了,不是由一个大方法统一处理所有逻辑,而是交给一组处理器按顺序处理。每个处理器只负责自己的职责,处理完后继续交给下一个处理器。

简单理解就是:

1
请求 -> 处理器 A -> 处理器 B -> 处理器 C -> 结束

比如在下单场景中,创建订单之前通常要做很多校验:

1
2
3
4
5
登录校验
库存校验
风控校验
优惠券校验
活动规则校验

如果全部写在一个方法里,就很容易变成一大堆 if else

责任链模式就是把这些逻辑拆成一个个独立节点:

1
LoginHandler -> StockHandler -> RiskHandler -> CouponHandler

每个节点只处理自己的事情。


二、为什么不用普通 if else?

假设我们有一个下单方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void createOrder(OrderContext context) {
if (context.getUserId() == null) {
throw new RuntimeException("用户未登录");
}

if (context.getBuyCount() <= 0) {
throw new RuntimeException("购买数量不合法");
}

if (!context.isRiskPass()) {
throw new RuntimeException("风控不通过");
}

if (!context.isCouponValid()) {
throw new RuntimeException("优惠券不可用");
}

// 创建订单
}

这种写法不是不能用,小项目里甚至很直接。

但是问题是:

1
2
3
4
1. 规则越来越多,方法会越来越长
2. 每次新增校验,都要改核心业务方法
3. 不同校验逻辑混在一起,不好维护
4. 某些校验想复用到别的业务里,不方便

责任链模式解决的不是“完全不用改代码”,而是:

1
2
3
4
把复杂逻辑拆成多个独立处理器
让每个处理器只负责一件事
让主流程保持干净
让新增规则尽量只新增类

三、先定义订单上下文

责任链里的每个节点都要处理同一个请求对象。

我们可以定义一个 OrderContext,表示这次下单请求携带的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class OrderContext {

private Long userId;
private Long productId;
private Integer buyCount;
private String couponCode;
private boolean riskPass;
private boolean couponValid;

public Long getUserId() {
return userId;
}

public Long getProductId() {
return productId;
}

public Integer getBuyCount() {
return buyCount;
}

public String getCouponCode() {
return couponCode;
}

public boolean isRiskPass() {
return riskPass;
}

public boolean isCouponValid() {
return couponValid;
}
}

这个 context 可以理解成:

1
这次请求的所有上下文信息

后面的登录校验、库存校验、风控校验都会从这个对象里面拿数据。


四、定义统一的 Handler 接口

所有责任链节点都实现同一个接口:

1
2
3
4
5
6
7
8
9
10
11
12
public interface OrderHandler {

/**
* 节点执行顺序
*/
int order();

/**
* 处理当前订单请求
*/
void handle(OrderContext context);
}

这里有两个方法。

第一个是:

1
int order();

它用来控制节点顺序。

比如:

1
2
3
4
LoginHandler  = 100
StockHandler = 200
RiskHandler = 300
CouponHandler = 400

第二个是:

1
void handle(OrderContext context);

它表示每个节点真正要执行的业务逻辑。


五、实现登录校验节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.stereotype.Component;

@Component
public class LoginHandler implements OrderHandler {

@Override
public int order() {
return 100;
}

@Override
public void handle(OrderContext context) {
if (context.getUserId() == null) {
throw new RuntimeException("用户未登录");
}

System.out.println("登录校验通过");
}
}

注意这里的:

1
@Component

它的意思是:

1
把 LoginHandler 交给 Spring 管理

只要这个类被 Spring 扫描到,它就会成为 Spring 容器里的一个 Bean。


六、实现库存校验节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.stereotype.Component;

@Component
public class StockHandler implements OrderHandler {

@Override
public int order() {
return 200;
}

@Override
public void handle(OrderContext context) {
if (context.getBuyCount() == null || context.getBuyCount() <= 0) {
throw new RuntimeException("购买数量不合法");
}

System.out.println("库存校验通过");
}
}

这个节点只负责库存相关校验。

它不关心登录,也不关心优惠券。


七、实现风控校验节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.stereotype.Component;

@Component
public class RiskHandler implements OrderHandler {

@Override
public int order() {
return 300;
}

@Override
public void handle(OrderContext context) {
if (!context.isRiskPass()) {
throw new RuntimeException("风控不通过");
}

System.out.println("风控校验通过");
}
}

如果风控不通过,直接抛异常。

一旦抛出异常,责任链后面的节点就不会继续执行了。


八、实现优惠券校验节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.stereotype.Component;

@Component
public class CouponHandler implements OrderHandler {

@Override
public int order() {
return 400;
}

@Override
public void handle(OrderContext context) {
if (context.getCouponCode() != null && !context.isCouponValid()) {
throw new RuntimeException("优惠券不可用");
}

System.out.println("优惠券校验通过");
}
}

到这里,我们已经有了四个节点:

1
2
3
4
LoginHandler
StockHandler
RiskHandler
CouponHandler

它们都实现了 OrderHandler 接口,也都加了 @Component


九、责任链核心类:OrderHandlerChain

接下来就是最关键的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.springframework.stereotype.Component;

import java.util.Comparator;
import java.util.List;

@Component
public class OrderHandlerChain {

private final List<OrderHandler> handlers;

public OrderHandlerChain(List<OrderHandler> handlers) {
this.handlers = handlers.stream()
.sorted(Comparator.comparingInt(OrderHandler::order))
.toList();
}

public void handle(OrderContext context) {
for (OrderHandler handler : handlers) {
handler.handle(context);
}
}
}

这段代码看起来简单,但里面有几个关键点。


十、为什么构造方法可以自动装配?

关键在这里:

1
public OrderHandlerChain(List<OrderHandler> handlers)

很多人第一次看会疑惑:

1
这个 List<OrderHandler> 是谁传进来的?

答案是:Spring 自动传进来的。

因为 OrderHandlerChain 上面有:

1
@Component

这说明 OrderHandlerChain 不是我们自己手动 new 的,而是交给 Spring 创建的。

Spring 创建这个对象时,会看它的构造方法需要什么参数。

它发现构造方法需要:

1
List<OrderHandler>

于是 Spring 就会去容器里找所有 OrderHandler 类型的 Bean。

比如容器里有:

1
2
3
4
LoginHandler
StockHandler
RiskHandler
CouponHandler

而且它们都实现了 OrderHandler 接口。

所以 Spring 会自动把它们收集起来,组成一个 List,然后传给构造方法。

等价于 Spring 在背后帮我们做了这件事:

1
2
3
4
5
6
7
8
List<OrderHandler> handlers = List.of(
loginHandler,
stockHandler,
riskHandler,
couponHandler
);

OrderHandlerChain chain = new OrderHandlerChain(handlers);

只不过这些代码不需要我们自己写。

所以这就是为什么构造方法可以自动装配。

前提有三个:

1
2
3
1. OrderHandlerChain 要交给 Spring 管理,也就是加 @Component
2. 每个 Handler 节点也要交给 Spring 管理,也就是加 @Component
3. 每个节点都要实现同一个接口 OrderHandler

十一、为什么要排序?

这段代码的作用是排序:

1
2
3
this.handlers = handlers.stream()
.sorted(Comparator.comparingInt(OrderHandler::order))
.toList();

因为 Spring 注入进来的 List<OrderHandler> 不一定是我们想要的业务顺序。

所以我们让每个 Handler 自己提供一个顺序值:

1
2
3
4
@Override
public int order() {
return 100;
}

然后按照 order() 从小到大排序。

最终得到的顺序就是:

1
2
3
4
LoginHandler   100
StockHandler 200
RiskHandler 300
CouponHandler 400

也就是:

1
登录校验 -> 库存校验 -> 风控校验 -> 优惠券校验

十二、for 循环执行是什么意思?

这段代码是责任链真正的执行入口:

1
2
3
4
5
public void handle(OrderContext context) {
for (OrderHandler handler : handlers) {
handler.handle(context);
}
}

它的意思是:

1
把 handlers 里面的每个处理器拿出来,按顺序执行它的 handle 方法

假设 handlers 排序后是:

1
2
3
4
LoginHandler
StockHandler
RiskHandler
CouponHandler

那么这段代码实际等价于:

1
2
3
4
loginHandler.handle(context);
stockHandler.handle(context);
riskHandler.handle(context);
couponHandler.handle(context);

执行流程就是:

1
2
3
4
5
6
7
8
9
订单请求进来

登录校验

库存校验

风控校验

优惠券校验

如果中间某个节点失败,比如库存不足:

1
throw new RuntimeException("库存不足");

那么后面的风控校验、优惠券校验就不会执行。

所以它可以实现:

1
前面的校验失败,后面的流程直接停止

十三、业务代码怎么使用责任链?

在订单服务里,只需要注入 OrderHandlerChain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.stereotype.Service;

@Service
public class OrderService {

private final OrderHandlerChain orderHandlerChain;

public OrderService(OrderHandlerChain orderHandlerChain) {
this.orderHandlerChain = orderHandlerChain;
}

public void createOrder(OrderContext context) {
// 先执行责任链校验
orderHandlerChain.handle(context);

// 校验全部通过后,继续创建订单
System.out.println("创建订单成功");
}
}

主流程变得很干净:

1
orderHandlerChain.handle(context);

这一句就代表:

1
执行下单前所有校验

具体有哪些校验,交给责任链内部处理。


十四、新增节点要不要改原来的代码?

这是责任链模式里很关键的问题。

如果使用最原始的手动责任链写法:

1
new LoginHandler(new StockHandler(new CouponHandler(null)));

那么新增节点确实要改装配代码。

但是在 Spring 项目里,我们通常使用自动装配写法。

比如现在要新增一个黑名单校验节点,只需要新增一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.springframework.stereotype.Component;

@Component
public class BlackListHandler implements OrderHandler {

@Override
public int order() {
return 250;
}

@Override
public void handle(OrderContext context) {
System.out.println("黑名单校验通过");
}
}

因为它:

1
2
3
1. 实现了 OrderHandler
2. 加了 @Component
3. 提供了 order 顺序

所以 Spring 启动时会自动把它加入:

1
List<OrderHandler> handlers

最终链路变成:

1
2
3
4
5
LoginHandler      100
StockHandler 200
BlackListHandler 250
RiskHandler 300
CouponHandler 400

我们不需要修改 OrderHandlerChain

这就是 Spring 自动装配责任链的价值。


十五、责任链是不是本质上就是 for 循环?

从代码落地上看,确实很像:

1
2
3
for (OrderHandler handler : handlers) {
handler.handle(context);
}

但是责任链模式强调的不是这个 for 循环本身,而是背后的设计思想:

1
2
3
4
多个处理器按顺序处理同一个请求
每个处理器只负责自己的职责
新增处理器时尽量不影响已有处理器
主业务流程不关心具体有哪些处理器

所以不要把责任链理解得太玄乎。

在实际工程里,它经常就是:

1
接口 + 多个实现类 + Spring 自动注入 List + 排序 + for 循环执行

这就是一个非常实用的责任链实现。


十六、责任链模式适合什么场景?

责任链模式适合这种场景:

1
2
3
4
5
一个请求需要经过多个处理步骤
每个步骤可以独立拆出来
步骤之间有明确顺序
中间某一步失败,可以终止后续执行
后续可能经常新增、删除、调整步骤

常见业务场景包括:

1
2
3
4
5
6
7
8
9
10
11
订单创建前校验
支付前校验
优惠券使用校验
拼团规则校验
风控规则校验
网关过滤器
登录认证
权限校验
Servlet Filter
Spring Interceptor
审批流程

比如拼团系统里,下单前可能有:

1
2
3
4
5
6
7
用户登录校验
商品状态校验
库存校验
拼团活动校验
用户参团资格校验
优惠券校验
风控校验

这些都可以拆成责任链节点。


十七、责任链模式的优点

责任链的优点主要有几个。

第一,减少大方法里的 if else。

原来所有逻辑都堆在一个方法里,现在拆成多个 Handler。

第二,职责更清晰。

每个 Handler 只做一件事。

第三,扩展更方便。

新增规则时,可以新增一个 Handler,而不是修改原来的大方法。

第四,主流程更干净。

订单服务只需要调用:

1
orderHandlerChain.handle(context);

不需要关心里面具体有哪些校验。

第五,可以配合 Spring 自动装配。

新增节点后,只要加 @Component,实现接口,就能自动加入链路。


十八、责任链模式的缺点

责任链也不是万能的。

它也有缺点。

第一,链路太长时,不好排查问题。

比如一个请求经过十几个 Handler,最后失败了,就要靠日志判断到底卡在哪个节点。

第二,顺序很重要。

如果顺序配置错,可能导致业务逻辑异常。

比如应该先校验登录,再校验用户资格。如果顺序反了,可能会出现空指针或者错误判断。

第三,不适合所有场景。

如果只是两三个简单判断,而且以后基本不会变,直接写 if else 可能更简单。

所以责任链适合规则多、变化多、需要扩展的场景。


十九、责任链和策略模式的区别

很多人会把责任链模式和策略模式混在一起。

可以这样区分:

1
2
策略模式:多个策略中选择一个执行
责任链模式:多个处理器按顺序执行

比如优惠活动:

1
2
3
满减
折扣
直减

如果一次只选择其中一种优惠策略,这更像策略模式。

但是下单前校验:

1
登录校验 -> 库存校验 -> 风控校验 -> 优惠券校验

这是多个节点按顺序执行,更像责任链模式。

一句话区分:

1
2
策略模式是“选一个”
责任链模式是“走一串”

二十、总结

责任链模式不是为了彻底消灭修改,也不是说用了它就一定比 if else 高级。

它真正解决的问题是:

1
2
3
4
5
把复杂流程拆成多个独立节点
让每个节点只负责自己的职责
让主业务流程保持简单
让新增规则尽量只新增类
让链路顺序可以统一管理

在 Spring 项目中,责任链常见写法就是:

1
2
3
4
5
定义统一 Handler 接口
多个 Handler 实现类加 @Component
在 Chain 类中注入 List<Handler>
按照 order 排序
for 循环依次执行

核心代码就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class OrderHandlerChain {

private final List<OrderHandler> handlers;

public OrderHandlerChain(List<OrderHandler> handlers) {
this.handlers = handlers.stream()
.sorted(Comparator.comparingInt(OrderHandler::order))
.toList();
}

public void handle(OrderContext context) {
for (OrderHandler handler : handlers) {
handler.handle(context);
}
}
}

这段代码的本质是:

1
2
3
Spring 自动收集所有责任链节点
按照顺序排好
请求来了之后,一个一个执行

所以,责任链模式可以简单记成一句话:

责任链 = 多个处理器排成一条链,按顺序处理同一个请求。