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:

  • 可读
  • 可重构
  • 少踩坑

相关推荐
晚烛4 小时前
CANN 调试工具与性能剖析:从日志分析到 NPU 行为追踪的完整调试体系
开发语言·windows·python·深度学习·缓存
huipeng9265 小时前
企业级微服务开发实战(一):项目启动与工程化设计
java·开发语言·spring boot·spring cloud·微服务·云原生·架构
解道Jdon9 小时前
[Budi插件:VsCode状态栏显示Copilot使用情况
ide·windows·git·svn·eclipse·github·visual studio
一个人旅程~10 小时前
如何避免在使用win安装U盘启动macbook时候出现键盘触摸板卡死的问题
windows·经验分享·macos·电脑
月走乂山11 小时前
Windows 10 WSL2 安装问题排查与解决全记录
windows·docker·hyper-v·故障排查·wsl2
老毛肚11 小时前
Spring boot 特性和自写Reids组件
java·spring boot·后端
jfqqqqq12 小时前
windows安装postgres的vector插件
windows
武子康12 小时前
Java-05 深入浅出 MyBatis动态SQL与参数拼接完全指南
java·spring boot·后端
默子昂13 小时前
用ai编写的一些小工具分享
windows·microsoft
Wilbert Lee14 小时前
OpenClaw Windows 最新官方安装教程(超简单一键安装)
windows·openclaw