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:

  • 可读
  • 可重构
  • 少踩坑

相关推荐
码出财富2 小时前
60万QPS下如何设计未读数系统
java·spring boot·spring cloud·java-ee
牵牛老人2 小时前
Windows下安装Qt后再添加或移除Qt组件需要组件的有效资料档案库如何处理
开发语言·windows·qt
玖釉-2 小时前
[Vulkan 学习之路] 17 - 拒绝摸鱼:多帧并行 (Frames in Flight)
c++·windows·图形渲染
这里是杨杨吖2 小时前
SpringBoot+Vue古建筑文化宣传交流系统 附带详细运行指导视频
vue.js·spring boot·系统·古建筑·文化宣传
高山上有一只小老虎2 小时前
解决springboot项目从mybatis切换为集成jpa后dao层方法检查爆红
java·spring boot
0和1的舞者2 小时前
SpringBoot 接口规范:统一返回、异常处理与拦截器详解
java·spring boot·后端·spring·知识·统一
一 乐2 小时前
动漫交流与推荐平台|基于springboot + vue动漫交流与推荐平台系统(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·后端
不染尘.2 小时前
Linux进程与服务管理
linux·运维·服务器·windows·centos·ssh
qq_12498707532 小时前
基于springboot的文化旅游小程序(源码+论文+部署+安装)
java·spring boot·后端·微信小程序·小程序·毕业设计·旅游