不知道你有没有遇到过这种情况:一个订单创建的接口,刚上线的时候只有几十行代码,逻辑也很简单。但随着业务迭代,三个月后代码就变得臃肿不堪:
public void createOrder(OrderRequest request) {
// 参数校验
if (request.getUserId() == null) {
throw new IllegalArgumentException("用户ID不能为空");
}
// ... 省略 20 行校验代码
// 权限校验
if (!checkPermission(request.getUserId())) {
throw new ForbiddenException("无权限");
}
// 业务校验
if (getOrderCount(request.getUserId()) >= 10) {
throw new BusinessException("订单数量超限");
}
// ... 省略 15 行业务校验
// 创建订单
Order order = buildOrder(request);
orderMapper.insert(order);
// 写操作日志
try {
operateLogService.log(order.getId(), "CREATE");
} catch (Exception e) {
// 日志失败不影响主流程
log.error("写日志失败", e);
}
// 发送通知
try {
notificationService.send(order);
} catch (Exception e) {
log.error("发送通知失败", e);
}
// 异步风控检查
asyncRiskCheckService.check(order);
}
几百行代码里,if、try-catch 到处都是。更让人头疼的是后续的维护问题:
- 日志写失败要不要抛异常?
- 通知发送失败要不要回滚订单?
- 新增一个“优惠券核销”逻辑该加在哪里?
- 测试环境想关闭风控检查又该怎么处理?
问题出在哪?
仔细分析上面的代码,它实际想完成的任务可以清晰地拆分为以下几个步骤:
1. 校验参数
2. 校验权限
3. 校验业务规则
4. 创建订单(核心)
5. 记录日志
6. 发送通知
7. 风控检查
这些步骤各有特点:
- 有的必须成功:比如参数校验、订单创建
- 有的可以失败:比如日志记录、通知发送
- 有的同步执行:比如各种校验
- 有的异步执行:比如风控检查
- 有的可能需要关闭:比如测试环境不需要风控
但传统的写法将所有逻辑都混杂在同一个方法里。步骤的顺序、失败后的处理策略、以及某个步骤是否可选,全都依赖代码中零散的 if 和 try-catch 来控制,代码自然会变得混乱且难以维护。
有没有更好的写法?
如果我们将每个步骤抽象成一个独立的“节点”,再用一个“管道”把它们有序地串联起来执行,情况会怎样?
首先,定义一个节点接口:
public interface PipelineNode<T> {
void execute(PipelineContext<T> context);
}
每个节点只专注于完成一件具体的事情,它不关心前后节点是谁,也不关心自己的执行序号。
然后,定义一个管道来负责调度和执行这些节点:
public class ExecutionPipeline<T> {
private final List<PipelineNode<T>> nodes;
public void execute(T data) {
PipelineContext<T> ctx = new PipelineContext<>(data);
for (PipelineNode<T> node : nodes) {
if (ctx.isInterrupted()) {
break;
}
node.execute(ctx);
}
}
}
现在,我们来看看用这种模式重写订单创建会是怎样的体验:
Pipeline pipeline = Pipeline.builder()
.add(new ParamValidateNode()) // 参数校验
.add(new PermissionCheckNode()) // 权限校验
.add(new BusinessValidateNode()) // 业务校验
.add(new CreateOrderNode()) // 创建订单
.add(new OperateLogNode()) // 写日志
.add(new NotificationNode()) // 发通知
.add(new AsyncRiskCheckNode()) // 风控检查
.build();
pipeline.execute(orderRequest);
看,主流程的核心代码被压缩到了寥寥数行,每一个业务逻辑都被封装在独立的、职责单一的节点中,清晰度大大提升。
这样做有什么好处?
1. 节点可插拔,灵活配置
想要新增一个“优惠券核销”逻辑?只需增加一个节点即可:
.add(new CouponDeductNode())
想根据环境动态开关某个功能(比如风控)?可以通过配置轻松控制:
.add(env.isProd() ? new RiskCheckNode() : new SkipNode())
2. 失败策略清晰明确
我们可以在 PipelineContext 中为每个节点定义清晰的失败处理策略:
public enum FailureStrategy {
CONTINUE, // 失败后继续执行后续节点
STOP, // 失败后中断整个管道
ROLLBACK // 失败后执行回滚(需配合事务管理)
}
例如,日志节点的失败策略可以设为 CONTINUE(日志失败不影响订单创建),而订单创建节点的失败策略则设为 STOP(创建失败则流程终止)。策略一目了然,不再需要散落的 try-catch 去判断。
3. 节点高度可复用
像参数校验、权限校验、操作日志记录这样的节点,其逻辑是通用的,可以被轻松复用到其他业务管道中,避免重复编码。
4. 执行顺序集中管控
节点的执行顺序完全由管道在组装时定义,每个节点内部无需关心顺序逻辑,这使得流程的编排和控制变得集中且直观。
执行管道:一种轻量级的工程化方案
上面介绍的方案,本质上是一种 执行管道(Execution Pipeline) 设计模式。它并非一个特定的框架,而是一种轻量级的工程设计思想:将一次业务执行拆分为多个可插拔、有明确顺序、且失败策略可控的阶段,并通过统一的机制将其串联执行。
它跟 AOP、责任链有什么区别?
你可能会联想到 AOP、责任链、过滤器等工作流引擎。它们确有相似之处,但侧重点不同:
| 方案 |
适用场景 |
局限性 |
| AOP |
通用的横切逻辑(如日志、事务) |
执行顺序不直观,难以做精细的流程控制和编排 |
| Filter/Interceptor |
HTTP 请求级别的预处理和后处理 |
通常局限于Web请求处理,难以用于普通业务方法 |
| @Async |
简单的异步执行 |
只解决并发问题,不解决流程的编排与步骤管理 |
| 责任链模式 |
需要动态组合处理链的场景 |
更偏重模式本身,缺少开箱即用的工程化封装(如上下文、失败策略) |
| 工作流引擎 |
跨系统、复杂且多变的业务流程 |
过于重型,对于单机应用内的流程编排来说,引入和维护成本较高 |
相比之下,执行管道可以看作是“工程化封装的责任链模式”。它汲取了责任链的思想,并在此基础上增加了上下文传递、统一失败策略、节点生命周期管理等工程实践,专门用于优雅地解决单机应用内部的复杂业务流程编排问题。
什么时候该考虑使用执行管道?
并非所有场景都需要引入执行管道。如果你的业务逻辑步骤固定、非常简单,或者流程虽然复杂但长期稳定不变,那么直接编码可能是更直接的选择。
但是,当你的代码出现以下“信号”时,就值得考虑引入执行管道模式了:
- 某个核心方法变得过长(超过百行),阅读和维护困难。
- 流程中包含多个“可选”步骤或“异步”执行步骤。
- 业务需要频繁地插入新的处理逻辑(如各种校验、钩子)。
- 不同运行环境(如测试、生产)需要不同的执行流程。
执行管道特别适合管理单机应用内的复杂核心业务流程,例如订单创建、用户注册、数据导入/导出、计费核算等场景。
重构建议与资源
优秀的代码架构往往是在持续重构中演进出来的,而非一蹴而就。如果你的项目中已经存在类似的“巨无霸”方法,不必急于全盘推翻。可以尝试从其中剥离出一个最独立的逻辑,将其改造成第一个管道节点,然后逐步迁移,让代码朝着清晰、可控的方向进化。
文中提到的执行管道设计模式,在 Spring Boot 项目中可以很好地与IoC容器结合,通过依赖注入来管理节点Bean。同时,这种对流程进行清晰拆解和编排的思想,也属于广义的 Pipeline 实践,值得深入探索。
对于想动手实践的开发者,可以参考示例代码仓库:
https://github.com/yuboon/java-examples/tree/master/springboot-pipeline
希望这种模式能帮助你更好地组织代码,让复杂的业务流程变得清晰、优雅且易于维护。在 云栈社区 也有更多关于架构设计和代码实践的讨论,欢迎交流。