Skip to content

责任链模式

责任链模式用于把一串处理步骤拆成多个处理器,让请求沿着链路依次处理。每个处理器只负责自己的职责,可以选择继续传递,也可以中断流程。

常见场景:登录校验、接口鉴权、风控规则、订单校验、网关过滤器、审批流、数据导入校验、Servlet Filter、Spring Security 过滤器链。

为什么需要责任链

假设创建订单前要做很多校验:

  1. 参数校验。
  2. 用户状态校验。
  3. 商品状态校验。
  4. 库存校验。
  5. 优惠券校验。
  6. 风控校验。

如果全部写在一个方法里:

java
public void createOrder(OrderCommand command) {
    checkParam(command);
    checkUser(command);
    checkProduct(command);
    checkStock(command);
    checkCoupon(command);
    checkRisk(command);
    saveOrder(command);
}

早期看起来没问题,但校验越来越多后会出现:

问题后果
方法越来越长主流程被校验细节淹没
新增规则总改老代码容易引入回归
校验顺序散落不同接口校验顺序不一致
部分规则需要开关if/else 越来越多
规则复用困难多个业务重复复制

责任链把每个规则拆成独立处理器。

总体流程

mermaid
flowchart TD
    A["请求进入"] --> B["参数校验处理器"]
    B --> C["用户校验处理器"]
    C --> D["库存校验处理器"]
    D --> E["风控校验处理器"]
    E --> F["业务执行"]

每个处理器只关心一件事。

责任链的执行模型

责任链的核心是“同一个请求对象,按确定顺序经过多个处理器”。它有三个关键点:

关键点解释为什么重要
上下文链路中共享的数据对象,例如订单、用户、库存、校验结果避免处理器之间互相直接依赖
顺序哪个处理器先执行,哪个后执行参数校验必须早于数据库查询,权限校验必须早于业务操作
中断策略失败后继续、跳过还是立即终止风控失败不能继续创建订单,日志失败通常不能影响主流程
mermaid
flowchart TD
    A["创建上下文"] --> B["按 order 排序处理器"]
    B --> C["执行当前处理器"]
    C --> D{"是否通过"}
    D -- "否" --> E["抛异常或返回失败"]
    D -- "是" --> F{"是否还有下一个"}
    F -- "有" --> C
    F -- "没有" --> G["链路通过"]

责任链不是把代码随便拆散,而是把“可插拔、可排序、可独立测试”的步骤拆出来。

Java Demo:订单校验责任链

上下文:

java
public class OrderContext {
    private final Long userId;
    private final Long skuId;
    private final int count;

    public OrderContext(Long userId, Long skuId, int count) {
        this.userId = userId;
        this.skuId = skuId;
        this.count = count;
    }

    public Long userId() {
        return userId;
    }

    public Long skuId() {
        return skuId;
    }

    public int count() {
        return count;
    }
}

处理器接口:

java
public interface OrderCheckHandler {
    void check(OrderContext context);

    int order();
}

参数校验:

java
@Component
public class ParamCheckHandler implements OrderCheckHandler {
    @Override
    public void check(OrderContext context) {
        if (context.userId() == null || context.skuId() == null || context.count() <= 0) {
            throw new IllegalArgumentException("订单参数不合法");
        }
    }

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

库存校验:

java
@Component
public class StockCheckHandler implements OrderCheckHandler {
    @Override
    public void check(OrderContext context) {
        int stock = 100;
        if (stock < context.count()) {
            throw new IllegalStateException("库存不足");
        }
    }

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

责任链执行器:

java
@Component
public class OrderCheckChain {
    private final List<OrderCheckHandler> handlers;

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

    public void check(OrderContext context) {
        for (OrderCheckHandler handler : handlers) {
            handler.check(context);
        }
    }
}

业务调用:

java
@Service
public class OrderService {
    private final OrderCheckChain checkChain;

    public OrderService(OrderCheckChain checkChain) {
        this.checkChain = checkChain;
    }

    public void createOrder(OrderContext context) {
        checkChain.check(context);
        // 校验通过后创建订单
    }
}

新增风控规则时,只要新增一个 OrderCheckHandler Bean,不需要修改 OrderService

可以中断的责任链

有些链路需要决定是否继续。

java
public interface Handler {
    boolean handle(Request request);
}
java
for (Handler handler : handlers) {
    boolean next = handler.handle(request);
    if (!next) {
        break;
    }
}

适合网关过滤、登录认证、权限校验:某个环节失败后直接中断。

商业场景:数据导入校验链

医疗数据采集与资产平台里,经常要导入设备资产、接口资产、数据库资产。导入不是简单保存文件,而是要经过一条稳定校验链:

顺序处理器作用不做会怎样
10文件格式校验判断扩展名、大小、表头后续解析报错,错误信息不友好
20必填字段校验检查资产编码、名称、科室入库后产生无效资产
30字典校验检查资产类型、状态、等级统计口径混乱
40唯一性校验检查资产编码是否重复覆盖历史数据或产生重复资产
50权限校验检查用户是否能导入对应科室造成越权写入
60风险标记标记敏感资产、异常资产后续治理无法识别风险数据
mermaid
flowchart TD
    A["上传资产文件"] --> B["文件格式校验"]
    B --> C["必填字段校验"]
    C --> D["字典值校验"]
    D --> E["唯一性校验"]
    E --> F["数据权限校验"]
    F --> G["风险标记"]
    G --> H["保存导入结果"]

这种场景用责任链的好处是:新增“敏感字段校验”时新增一个处理器,不需要改导入主流程;某个处理器失败时能明确知道失败在哪一步。

Demo:可返回错误集合的校验链

有些导入任务不希望遇到第一个错误就停止,而是希望一次返回所有错误。这时可以让处理器往上下文里追加错误。

java
public class ImportContext {
    private final List<Map<String, String>> rows;
    private final List<String> errors = new ArrayList<>();

    public ImportContext(List<Map<String, String>> rows) {
        this.rows = rows;
    }

    public List<Map<String, String>> getRows() {
        return rows;
    }

    public void addError(String error) {
        errors.add(error);
    }

    public boolean hasError() {
        return !errors.isEmpty();
    }

    public List<String> getErrors() {
        return errors;
    }
}
java
public interface ImportCheckHandler {
    int order();

    void check(ImportContext context);
}
java
@Component
public class RequiredFieldCheckHandler implements ImportCheckHandler {
    @Override
    public int order() {
        return 20;
    }

    @Override
    public void check(ImportContext context) {
        for (int i = 0; i < context.getRows().size(); i++) {
            Map<String, String> row = context.getRows().get(i);
            if (row.get("assetCode") == null || row.get("assetCode").isBlank()) {
                context.addError("第 " + (i + 1) + " 行资产编码不能为空");
            }
        }
    }
}
java
@Component
public class ImportCheckChain {
    private final List<ImportCheckHandler> handlers;

    public ImportCheckChain(List<ImportCheckHandler> handlers) {
        this.handlers = handlers.stream()
                .sorted(Comparator.comparingInt(ImportCheckHandler::order))
                .collect(Collectors.toList());
    }

    public void check(ImportContext context) {
        for (ImportCheckHandler handler : handlers) {
            handler.check(context);
        }
    }
}

使用:

java
ImportContext context = new ImportContext(rows);
importCheckChain.check(context);
if (context.hasError()) {
    throw new IllegalArgumentException(String.join(",", context.getErrors()));
}

如果是支付、扣库存、创建订单这类强一致流程,通常遇到错误就中断;如果是 Excel 导入校验,通常更适合收集错误后统一返回。

Spring 中的责任链

Servlet Filter

mermaid
flowchart TD
    A["HTTP 请求"] --> B["Filter 1"]
    B --> C["Filter 2"]
    C --> D["Filter 3"]
    D --> E["Servlet / Controller"]

每个 Filter 都可以在请求前后处理,也可以不调用 chain.doFilter 来中断。

Spring Security

Spring Security 的核心就是一组过滤器链,例如认证、会话、CSRF、授权、异常处理等。每个过滤器负责一个安全职责。

简化链路可以这样理解:

mermaid
flowchart TD
    A["HTTP 请求"] --> B["SecurityContext 处理"]
    B --> C["登录认证过滤器"]
    C --> D["异常处理过滤器"]
    D --> E["授权过滤器"]
    E --> F{"是否允许访问"}
    F -- "否" --> G["返回 401 或 403"]
    F -- "是" --> H["进入 Controller"]

这里每个过滤器就是一个处理器。认证失败可以中断,授权失败可以中断,只有通过后才进入业务接口。

和策略模式区别

对比责任链策略模式
关注点多个处理器按顺序协作多个算法选一个执行
执行数量通常执行多个通常执行一个
是否有顺序有顺序一般无链式顺序
场景校验链、过滤器链、审批链支付、折扣、解析

商业场景

医疗数据导入可以用责任链做数据校验:

  1. 必填字段校验。
  2. 字典值校验。
  3. 资产编码唯一性校验。
  4. 科室存在性校验。
  5. 数据权限校验。
  6. 风险数据标记。

每个校验器独立,新增规则不会改导入主流程。

常见坑

问题后果建议
链顺序不明确结果不稳定给处理器明确 order
处理器互相依赖解耦失败通过上下文传递必要数据
链太长但无日志排查不知道卡在哪每个处理器记录关键结果
所有逻辑都塞责任链主流程割裂只把可插拔步骤放入链
异常策略不统一有的中断,有的吞异常明确失败是否继续

生产排查方法

责任链一旦用于商业项目,必须能排查“到底卡在哪个处理器”。建议至少记录这些字段:

字段说明
traceId一次请求的链路编号
chainName链名称,例如 orderCreateCheckChain
handlerName当前处理器名称
order当前处理器顺序
costMs当前处理器耗时
result通过、失败、跳过
errorCode失败原因编码

排查流程:

mermaid
flowchart TD
    A["发现接口失败或变慢"] --> B["按 traceId 查链路日志"]
    B --> C["定位失败或耗时最高的 handler"]
    C --> D{"是业务失败还是系统异常"}
    D -- "业务失败" --> E["检查入参、配置、权限、数据状态"]
    D -- "系统异常" --> F["检查依赖服务、数据库、缓存、网络"]
    E --> G["补充测试或修复规则"]
    F --> G

如果链路没有日志,责任链会比普通代码更难排查,因为调用栈被拆散了。

面试标准回答

text
责任链模式把多个处理步骤拆成一组处理器,请求沿链依次经过处理器。它适合校验链、过滤器链、审批链等场景。优点是每个处理器职责单一,新增规则可以新增处理器,减少主流程修改;缺点是链路过长时排查困难,必须明确顺序、异常策略和日志。Servlet Filter 和 Spring Security 过滤器链都是典型应用。

追问补充:

追问回答方向
责任链一定会执行所有处理器吗不一定。校验收集型可以全部执行,认证鉴权型通常失败就中断
怎么保证顺序使用 order@Order 或配置文件排序,不要依赖 Bean 注入顺序
链路太长怎么办拆分链路、记录每个处理器日志、只把可插拔步骤放进链
和过滤器有什么关系Servlet Filter、Gateway Filter、Spring Security Filter 都是责任链思想

本章小结

责任链解决的是“多个步骤按顺序处理同一个请求”。它不是为了炫技拆类,而是当规则会持续增加、顺序可定义、每个规则相对独立时,用链路把变化点隔离出来。