Spring Boot 中优雅地使用责任链模式(@Order 实战)

文章目录

当 if-else 开始失控时,责任链是你最该想到的设计模式。

一、背景:if-else

在实际业务中,我们经常遇到这样的流程:

  • 参数校验
  • 权限校验
  • 幂等 / 状态校验
  • 核心业务处理
  • 资源操作(文件、数据库、远程调用)
  • 回调 / 通知

最初代码可能是这样的:

java 复制代码
if (!validParam(req)) {
    return error("参数错误");
}
if (!hasPermission(user, doc)) {
    return error("无权限");
}
if (!stateOk(doc)) {
    return error("状态不允许");
}
// 生成 PDF
// 上传文件
// 发 MQ

问题很明显:

  • if-else 越来越长
  • 顺序强耦合
  • 新增步骤要改"主流程"
  • 可读性、可维护性迅速下降

这正是责任链模式最适合出场的地方。


二、什么是责任链模式

责任链模式:把一件事交给一串处理者,一个处理不了就交给下一个。

现实中的例子:

  • 请假审批:组长 → 经理 → 总监
  • 安检流程:初检 → 复检 → 放行
  • Web 请求:Filter → Interceptor → Controller

三、Spring Boot 为什么特别适合责任链

Spring 本身就天然支持"链式处理":

  • FilterChain
  • HandlerInterceptor
  • Bean 注入 List<T>
  • @Order 控制顺序

👉 不用自己写 setNext,Spring 帮你把链排好


四、核心设计思路

三个核心角色

角色 作用
Context 在链条中传递数据
Handler 每个步骤只做一件事
Chain 按顺序执行所有 Handler

执行效果

java 复制代码
exportChain.execute(context);

而不是一堆 if-else。


五、实战:Spring Boot + 责任链(@Order)

请求 / 响应 DTO

csharp 复制代码
public class ExportRequest {
    private String docId;
    private String userId;

    public String getDocId() { return docId; }
    public void setDocId(String docId) { this.docId = docId; }

    public String getUserId() { return userId; }
    public void setUserId(String userId) { this.userId = userId; }
}
csharp 复制代码
public class ExportResponse {
    private String pdfUrl;

    public ExportResponse(String pdfUrl) {
        this.pdfUrl = pdfUrl;
    }

    public String getPdfUrl() {
        return pdfUrl;
    }
}

Context:贯穿整个链路

java 复制代码
public class ExportContext {

    private String docId;
    private String userId;

    private byte[] pdfBytes;
    private String pdfUrl;

    public String getDocId() { return docId; }
    public void setDocId(String docId) { this.docId = docId; }

    public String getUserId() { return userId; }
    public void setUserId(String userId) { this.userId = userId; }

    public byte[] getPdfBytes() { return pdfBytes; }
    public void setPdfBytes(byte[] pdfBytes) { this.pdfBytes = pdfBytes; }

    public String getPdfUrl() { return pdfUrl; }
    public void setPdfUrl(String pdfUrl) { this.pdfUrl = pdfUrl; }
}

Handler 接口

java 复制代码
public interface ExportHandler {
    void handle(ExportContext ctx);
}

核心:Chain(Spring 自动注入)

java 复制代码
@Component
public class ExportChain {

    private final List<ExportHandler> handlers;

    public ExportChain(List<ExportHandler> handlers) {
        this.handlers = handlers;
    }

    public void execute(ExportContext context) {
        for (ExportHandler handler : handlers) {
            System.out.println("执行:" + handler.getClass().getSimpleName());
            handler.handle(context);
        }
    }
}

⚠️ 关键点:

Spring 会按照 @Order 自动排序后,再注入 List


六、@Order:责任链的"灵魂"

规则只有一条

text 复制代码
数字越小,越先执行

各个处理节点

参数校验

java 复制代码
@Component
@Order(10)
public class ValidateHandler implements ExportHandler {

    @Override
    public void handle(ExportContext context) {
        System.out.println("👉 ValidateHandler");
        if (context.getDocId() == null) {
            throw new RuntimeException("docId 不能为空");
        }
    }
}

权限校验

java 复制代码
@Component
@Order(20)
public class AuthHandler implements ExportHandler {

    @Override
    public void handle(ExportContext context) {
        System.out.println("👉 AuthHandler");
        // 模拟鉴权
    }
}

生成 PDF

java 复制代码
@Component
@Order(40)
public class GeneratePdfHandler implements ExportHandler {

    @Override
    public void handle(ExportContext context) {
        System.out.println("👉 GeneratePdfHandler");
        context.setPdfBytes("PDF DATA".getBytes());
    }
}

上传文件

java 复制代码
@Component
@Order(50)
public class UploadHandler implements ExportHandler {

    @Override
    public void handle(ExportContext context) {
        System.out.println("👉 UploadHandler");
        context.setPdfUrl("http://example.com/demo.pdf");
    }
}

不需要任何手工组装。


七、Controller 里会变得多干净

java 复制代码
  private final ExportChain exportChain;

    public FileController(ExportChain exportChain) {
        this.exportChain = exportChain;
    }


    @PostMapping("/export/pdf")
    public ExportResponse export(@RequestBody ExportRequest req) {

        ExportContext ctx = new ExportContext();
        ctx.setDocId(req.getDocId());
        ctx.setUserId(req.getUserId());

        exportChain.execute(ctx);

        return new ExportResponse(ctx.getPdfUrl());
    }

👉 Controller 只负责"发起流程",不关心细节
实际执行顺序


八、为什么这种写法特别适合真实项目

优点总结

  • 消灭 if-else

  • 职责清晰,每个 Handler 只干一件事

  • 新增 / 删除步骤不影响主流程

  • 顺序清楚,代码即文档

  • 非常适合:

    • 导出流程
    • 审批流
    • 校验流水线
    • 中间件式处理

责任链模式不是"高级设计",
而是让复杂流程重新变得像流水线一样简单。

九、最佳实践建议

Order 编号建议

text 复制代码
10  参数校验
20  权限 / 租户
30  幂等 / 状态
40  核心业务
50  IO / 远程调用
60  通知 / 回调

中间预留空位,方便插节点。


用异常中断链条

比 return boolean 更清晰:

java 复制代码
throw new BizException("状态不允许");

Context 别用 Map

强类型 Context:

  • 可读
  • 可重构
  • 少踩坑

相关推荐
用户908324602732 小时前
Spring AI 1.1.2 + Neo4j:用知识图谱增强 RAG 检索(上篇:图谱构建)
java·spring boot
用户83071968408221 小时前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
Java水解1 天前
Spring Boot 视图层与模板引擎
spring boot·后端
Java水解1 天前
一文搞懂 Spring Boot 默认数据库连接池 HikariCP
spring boot·后端
洋洋技术笔记1 天前
Spring Boot Web MVC配置详解
spring boot·后端
初次攀爬者2 天前
Kafka 基础介绍
spring boot·kafka·消息队列
用户8307196840822 天前
spring ai alibaba + nacos +mcp 实现mcp服务负载均衡调用实战
spring boot·spring·mcp
Java水解2 天前
SpringBoot3全栈开发实战:从入门到精通的完整指南
spring boot·后端
阿白的白日梦2 天前
winget基础管理---更新/修改源为国内源
windows
初次攀爬者3 天前
RocketMQ在Spring Boot上的基础使用
java·spring boot·rocketmq