注解式条件工作流实战:基于Spring Boot实现订单审批动态路由

注解式条件工作流实战:基于Spring Boot实现订单审批动态路由 在业务系统里审批流、工单流转、发布流程这类场景几乎都会遇到同一个问题流程走到某个节点时并不是无脑往下走而是要根据订单金额、用户角色、业务状态等条件动态决定下一步。很多人一开始用 if-else 硬写等条件一多代码就变得很难维护。本文围绕条件工作流的注解写法展开先讲清楚条件工作流的核心概念再通过一个完整的订单审批案例从零实现一套注解式条件工作流引擎包含注解定义、条件评估、动态跳过、测试用例和常见坑点。1. 背景与核心概念1.1 什么是条件工作流条件工作流简单理解就是一条流程流水线上的每个节点都有开关只有当条件满足时当前节点才会被路由和执行。这里的“条件”可以是订单金额、用户角色、审批状态、外部接口返回结果等任何运行时数据。从专业一点的角度看条件工作流是指工作流引擎在流转过程中根据一组规则评估当前上下文数据从而决定流程分支走向的机制。它解决的核心问题是流程不是一条直线走到底而是“动态路由”。条件工作流在真实业务中非常多见举几个例子订单金额小于 1000 元自动通过大于等于 1000 元进入经理审批大于等于 5000 元还要追加总监审批。工单类型为“线上故障”时自动通知值班人员工单类型为“需求变更”时进入产品评审节点。风控规则中用户命中高风控名单时订单进入人工复核节点否则直接放行。这些场景的共同特征是节点是否能执行取决于一组条件判断。如果流程节点数量少用 if-else 还能撑住一旦节点数和条件数增加代码可读性、可扩展性会迅速恶化。这时就需要一套条件工作流的抽象层来解决。1.2 为什么需要注解写法传统实现条件流程通常有三种做法第一种是直接在业务代码中写 if-else。if (order.getAmount() 1000) { managerApprove(order); } if (order.getAmount() 5000) { directorApprove(order); } finish(order);这种写法最直观但也是最难维护的。当流程节点多、条件重叠、需要记录节点执行历史时if-else 会越写越乱。第二种是使用模板方法模式把每个流程定义成子类。这种方式比 if-else 清晰但如果流程数量多会造成“子类爆炸”维护成本也不低。第三种是引入 BPMN 工作流引擎用 XML 或可视化编排器定义流程。这种方式功能强大适合复杂长流程。但对于中轻量级业务场景来说引入一套完整引擎的部署成本和运维成本都比较高。注解写法介于“硬编码”和“重量级引擎”之间。它的核心思路是用注解描述流程节点、执行顺序和条件规则然后在运行时通过反射统一解析执行。这样做的优势很明显流程逻辑与业务代码内聚一个工作流类对应一条流程。条件以声明式表达可读性比 if-else 高很多。天然融入 Spring 容器可以使用依赖注入、事务、切面等能力。相比 BPMN XML注解写法更轻量适合嵌入到现有业务系统中。1.3 注解写法与其他方案对比对比维度if-else 硬编码注解式条件工作流BPMN 工作流引擎流程可视化无无但节点注解较直观强支持在线编排学习成本低中高侵入性低低较高动态变更需改代码需改代码可在线调整流程适用场景极简单流程中轻量级流程复杂长流程需要说明的是注解写法并不是要取代 BPMN 引擎而是为“流程没那么复杂、又不想引入重引擎”的业务提供一种更轻的落地方式。如果你是学生练手或者项目中只是需要简单审批流这套思路完全够用如果流程复杂度已经上升到需要可视化编排和动态发布那直接上 Flowable 或 Activiti 更合适。2. 环境准备与版本说明2.1 运行环境本文示例以 Spring Boot 为主核心依赖是 Spring Context 和 Spring ExpressionSpEL不依赖数据库和 Web 容器因此环境准备非常简单。JDK 8 及以上版本。Maven 3.6 及以上版本。Spring Boot 2.7.x 或 3.x本文示例以 Spring Boot 2.7.18 为例。IDE 使用 IntelliJ IDEA 或 Eclipse 均可。如果使用 Spring Boot 3.x需要注意依赖包名从javax切换为jakarta但本文示例主要使用spring-boot-starter和spring-boot-starter-test这两个依赖在 2.x 和 3.x 中都是支持的核心代码无需改动。2.2 项目结构创建 Maven 项目后目录结构规划如下condition-workflow-demo ├── pom.xml └── src/main/java/com/example/workflow ├── WorkflowApplication.java ├── annotation │ ├── Workflow.java │ ├── WorkflowNode.java │ └── ConditionEvaluator.java ├── context │ └── WorkflowContext.java ├── engine │ └── WorkflowEngine.java ├── condition │ └── ManagerApproveCondition.java ├── workflow │ └── OrderApprovalWorkflow.java └── model └── OrderInfo.java测试类放在src/test/java/com/example/workflow/OrderApprovalWorkflowTest.java2.3 Maven 依赖pom.xml只需要引入 Spring Boot 基础依赖代码量很小。?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent groupIdcom.example/groupId artifactIdcondition-workflow-demo/artifactId version1.0.0-SNAPSHOT/version namecondition-workflow-demo/name properties java.version1.8/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build /project如果你的本地仓库没有2.7.18这个版本可以换成其他2.7.x小版本核心逻辑不受影响。3. 注解式条件工作流的核心设计3.1 注解体系与角色划分设计一套注解式条件工作流先要明确注解的职责。本文定义三个核心组件Workflow类级别注解标注某个类是一个工作流定义。WorkflowNode方法级别注解标注工作流中的一个节点并声明节点名称、执行顺序和条件。ConditionEvaluator条件评估接口用来承载复杂的 Java 条件判断逻辑。这种设计的核心思想是“元数据驱动执行”。工作流类本身只负责声明节点方法节点的执行顺序和条件规则全部写在注解上统一由WorkflowEngine解析执行。3.2 Spring 的 Conditional 与条件工作流的区别这里要特别区分一个容易混淆的概念Spring 框架自带的Conditional注解和本文讲的“条件工作流注解”虽然名字相近但层级完全不同。Conditional作用于 Spring Bean 的创建过程用于决定某个 Bean 是否被注入容器属于容器装配层面的条件判断。条件工作流注解作用于业务方法执行过程用于决定流程中的某个节点是否执行属于业务流转层面的条件判断。Conditional在容器启动阶段就会被解析而流程节点条件是在每次执行工作流时动态判断。所以不能用 Spring 的Conditional直接实现流程节点路由两者解决的问题不一样。3.3 条件评估的两条路对于注解中的条件字段我提供了两种写法目的是让开发者根据场景灵活选择。第一种是实现ConditionEvaluator接口把条件判断逻辑写进独立的 Java 类。这种方式适合复杂条件比如需要查数据库、调用外部接口、组合多个判断因子等。第二种是使用 SpEL 表达式直接写在注解的condition属性里。这种方式适合简单规则比如金额比较、状态判断代码写起来很短。当conditionClass和condition同时存在时引擎约定优先使用conditionClass。这个优先级顺序要在设计文档里写清楚否则团队成员会误用。3.4 WorkflowEngine 的执行流程WorkflowEngine是执行核心它的执行流程可以拆成下面几步校验传入的工作流对象是否带有Workflow注解。反射获取类中所有带有WorkflowNode注解的方法。按注解的order属性升序排序保证节点顺序稳定。遍历节点方法先评估当前节点的条件。条件满足时调用节点方法条件不满足时记录日志并跳过。所有节点遍历完成后工作流执行结束。在此基础上还可以扩展节点状态记录、执行耗时统计、异常补偿等能力。但核心骨架就是反射收集方法、排序、条件判断、执行节点。4. 完整实战案例订单审批条件工作流4.1 需求分析我们用订单审批流程作为案例。业务规则如下所有订单先进入“提交申请”节点。金额小于 1000 元不需要审批直接到“审批完成”节点。金额大于等于 1000 元进入“经理审批”节点。金额大于等于 5000 元进入“总监审批”节点。审批完成后统一进入“审批完成”节点。把规则翻译成条件模型就是三个关键节点节点名称执行条件提交申请始终执行经理审批order.amount 1000总监审批order.amount 5000审批完成始终执行这个例子能很好地体现条件工作流的价值不同金额的订单走的是不同长度的流程链。4.2 编写注解定义首先是Workflow注解用来标注一个工作流类。// 文件路径src/main/java/com/example/workflow/annotation/Workflow.java package com.example.workflow.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) Documented public interface Workflow { String name(); }然后是WorkflowNode注解用来标注节点方法。// 文件路径src/main/java/com/example/workflow/annotation/WorkflowNode.java package com.example.workflow.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) Documented public interface WorkflowNode { String name(); int order() default 0; String condition() default ; Class? extends ConditionEvaluator[] conditionClass() default {}; }最后是ConditionEvaluator条件评估接口。// 文件路径src/main/java/com/example/workflow/annotation/ConditionEvaluator.java package com.example.workflow.annotation; import com.example.workflow.context.WorkflowContext; public interface ConditionEvaluator { boolean evaluate(WorkflowContext context); }这里要注意注解的RetentionPolicy必须设为RUNTIME否则在程序运行时无法通过反射读取注解信息。Target也必须要精确Workflow只允许标注在类型上WorkflowNode只允许标注在方法上。4.3 编写工作流上下文工作流执行过程中需要携带当前数据比如订单对象、执行节点记录。我设计了一个WorkflowContext内部用 Map 存放业务数据同时维护一个节点执行记录列表。// 文件路径src/main/java/com/example/workflow/context/WorkflowContext.java package com.example.workflow.context; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class WorkflowContext { private final MapString, Object data new HashMap(); private final ListString stepRecords new ArrayList(); private String currentNode; public void put(String key, Object value) { data.put(key, value); } public Object get(String key) { return data.get(key); } public T T get(String key, ClassT type) { return type.cast(data.get(key)); } public MapString, Object asMap() { return data; } public void recordStep(String nodeName) { stepRecords.add(nodeName); } public ListString getStepRecords() { return new ArrayList(stepRecords); } public String getCurrentNode() { return currentNode; } public void setCurrentNode(String currentNode) { this.currentNode currentNode; } }stepRecords是排查问题时的关键数据。通过它可以看到一个订单实际执行了哪些节点哪些节点被条件跳过。4.4 编写订单模型// 文件路径src/main/java/com/example/workflow/model/OrderInfo.java package com.example.workflow.model; public class OrderInfo { private String orderNo; private double amount; public OrderInfo(String orderNo, double amount) { this.orderNo orderNo; this.amount amount; } public String getOrderNo() { return orderNo; } public void setOrderNo(String orderNo) { this.orderNo orderNo; } public double getAmount() { return amount; } public void setAmount(double amount) { this.amount amount; } }4.5 编写条件类本案例中经理审批的条件用ConditionEvaluator实现方便展示复杂条件的写法。// 文件路径src/main/java/com/example/workflow/condition/ManagerApproveCondition.java package com.example.workflow.condition; import com.example.workflow.annotation.ConditionEvaluator; import com.example.workflow.context.WorkflowContext; import com.example.workflow.model.OrderInfo; import org.springframework.stereotype.Component; Component public class ManagerApproveCondition implements ConditionEvaluator { Override public boolean evaluate(WorkflowContext context) { OrderInfo order context.get(order, OrderInfo.class); return order ! null order.getAmount() 1000; } }总监审批的条件相对简单我就直接用 SpEL 表达式来写展示注解写法的第二种条件形式。4.6 编写工作流定义类这是整个案例的核心类。通过注解就能直观看出流程节点、顺序和条件。// 文件路径src/main/java/com/example/workflow/workflow/OrderApprovalWorkflow.java package com.example.workflow.workflow; import com.example.workflow.annotation.Workflow; import com.example.workflow.annotation.WorkflowNode; import com.example.workflow.condition.ManagerApproveCondition; import com.example.workflow.context.WorkflowContext; import com.example.workflow.model.OrderInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; Component Workflow(name orderApprovalWorkflow) public class OrderApprovalWorkflow { private static final Logger log LoggerFactory.getLogger(OrderApprovalWorkflow.class); WorkflowNode(name 提交申请, order 1) public void submitNode(WorkflowContext context) { OrderInfo order context.get(order, OrderInfo.class); log.info(节点[提交申请]执行订单号: {}, 金额: {}, order.getOrderNo(), order.getAmount()); context.recordStep(submit); } WorkflowNode(name 经理审批, order 2, conditionClass ManagerApproveCondition.class) public void managerApproveNode(WorkflowContext context) { log.info(节点[经理审批]执行金额 1000需要经理审批); context.recordStep(managerApprove); } WorkflowNode(name 总监审批, order 3, condition #order.amount 5000) public void directorApproveNode(WorkflowContext context) { log.info(节点[总监审批]执行金额 5000需要总监审批); context.recordStep(directorApprove); } WorkflowNode(name 审批完成, order 4) public void finishNode(WorkflowContext context) { log.info(节点[审批完成]执行流程结束); context.recordStep(finish); } }注意一下总监审批节点上的条件写法condition #order.amount 5000。这个表达式中的#order是WorkflowContext中存放的订单对象变量名引擎执行时会从上下文中取出并解析。如果有读者不习惯 SpEL 表达式总监节点也可以换成conditionClass DirectorApproveCondition.class效果完全一样。这也是注解写法的优势同一个流程条件表达方式可以灵活替换。4.7 编写核心引擎接下来是条件工作流引擎的核心代码负责读注解、排顺序、判断条件、执行方法。// 文件路径src/main/java/com/example/workflow/engine/WorkflowEngine.java package com.example.workflow.engine; import com.example.workflow.annotation.ConditionEvaluator; import com.example.workflow.annotation.Workflow; import com.example.workflow.annotation.WorkflowNode; import com.example.workflow.context.WorkflowContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Comparator; import java.util.List; Component public class WorkflowEngine { private static final Logger log LoggerFactory.getLogger(WorkflowEngine.class); private final ApplicationContext applicationContext; private final SpelExpressionParser parser new SpelExpressionParser(); public WorkflowEngine(ApplicationContext applicationContext) { this.applicationContext applicationContext; } public void execute(Object workflowBean, WorkflowContext context) { Class? clazz workflowBean.getClass(); Workflow workflow clazz.getAnnotation(Workflow.class); if (workflow null) { throw new IllegalArgumentException(clazz.getName() 缺少 Workflow 注解); } ListMethod nodeMethods new ArrayList(); for (Method method : clazz.getDeclaredMethods()) { if (method.isAnnotationPresent(WorkflowNode.class)) { nodeMethods.add(method); } } nodeMethods.sort(Comparator.comparingInt( method - method.getAnnotation(WorkflowNode.class).order() )); log.info(开始执行工作流: {}, workflow.name()); for (Method method : nodeMethods) { WorkflowNode node method.getAnnotation(WorkflowNode.class); if (!evaluateCondition(node, context)) { log.info(节点[{}] 条件不满足跳过, node.name()); continue; } context.setCurrentNode(node.name()); try { method.invoke(workflowBean, context); } catch (Exception e) { throw new RuntimeException(执行节点[ node.name() ]失败, e); } } log.info(工作流执行完成: {}, workflow.name()); } private boolean evaluateCondition(WorkflowNode node, WorkflowContext context) { if (node.conditionClass().length 0) { for (Class? extends ConditionEvaluator conditionClass : node.conditionClass()) { ConditionEvaluator evaluator applicationContext.getBean(conditionClass); if (!evaluator.evaluate(context)) { return false; } } return true; } if (StringUtils.hasText(node.condition())) { StandardEvaluationContext spelContext new StandardEvaluationContext(); context.asMap().forEach(spelContext::setVariable); Boolean result parser.parseExpression(node.condition()) .getValue(spelContext, Boolean.class); return Boolean.TRUE.equals(result); } return true; } }引擎中有一个容易踩坑的点method.invoke(workflowBean, context)的第二个参数是WorkflowContext所以节点方法必须声明为public void methodName(WorkflowContext context)的形式参数类型必须匹配。如果参数不匹配反射调用会抛异常。另一个细节是 SpEL 表达式中变量的注入逻辑。context.asMap().forEach(spelContext::setVariable)会把WorkflowContext里的所有数据作为变量放入 SpEL 上下文所以表达式里写#order.amount时order就是context.put(order, order)时使用的 key。4.8 编写启动类和测试用例启动类相对简单就是一个标准的 Spring Boot 入口。// 文件路径src/main/java/com/example/workflow/WorkflowApplication.java package com.example.workflow; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class WorkflowApplication { public static void main(String[] args) { SpringApplication.run(WorkflowApplication.class, args); } }测试类是验证条件工作流是否正确的关键一环。我设计了三个测试用例分别对应小订单、中订单、大订单。// 文件路径src/test/java/com/example/workflow/OrderApprovalWorkflowTest.java package com.example.workflow; import com.example.workflow.context.WorkflowContext; import com.example.workflow.engine.WorkflowEngine; import com.example.workflow.model.OrderInfo; import com.example.workflow.workflow.OrderApprovalWorkflow; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; SpringBootTest class OrderApprovalWorkflowTest { Autowired private WorkflowEngine workflowEngine; Autowired private OrderApprovalWorkflow orderApprovalWorkflow; Test void smallOrderShouldSkipManagerAndDirector() { WorkflowContext context new WorkflowContext(); context.put(order, new OrderInfo(SO001, 800)); workflowEngine.execute(orderApprovalWorkflow, context); ListString steps context.getStepRecords(); System.out.println(小订单执行节点: steps); assertTrue(steps.contains(submit)); assertTrue(steps.contains(finish)); assertFalse(steps.contains(managerApprove)); assertFalse(steps.contains(directorApprove)); } Test void middleOrderShouldIncludeManagerOnly() { WorkflowContext context new WorkflowContext(); context.put(order, new OrderInfo(SO002, 3000)); workflowEngine.execute(orderApprovalWorkflow, context); ListString steps context.getStepRecords(); System.out.println(中订单执行节点: steps); assertTrue(steps.contains(submit)); assertTrue(steps.contains(managerApprove)); assertTrue(steps.contains(finish)); assertFalse(steps.contains(directorApprove)); } Test void largeOrderShouldIncludeAllNodes() { WorkflowContext context new WorkflowContext(); context.put(order, new OrderInfo(SO003, 8000)); workflowEngine.execute(orderApprovalWorkflow, context); ListString steps context.getStepRecords(); System.out.println(大订单执行节点: steps); assertTrue(steps.contains(submit)); assertTrue(steps.contains(managerApprove)); assertTrue(steps.contains(directorApprove)); assertTrue(steps.contains(finish)); } }4.9 运行与验证在项目根目录执行测试命令mvn test如果集成在 IDEA 中也可以直接运行OrderApprovalWorkflowTest测试类。预期输出中大订单用例的日志大致如下开始执行工作流: orderApprovalWorkflow 节点[提交申请]执行订单号: SO003, 金额: 8000.0 节点[经理审批]执行金额 1000需要经理审批 节点[总监审批]执行金额 5000需要总监审批 节点[审批完成]执行流程结束 工作流执行完成: orderApprovalWorkflow小订单用例则会看到“经理审批”“总监审批”两个节点被条件跳过日志中会出现节点[经理审批] 条件不满足跳过 节点[总监审批] 条件不满足跳过这说明条件工作流已经按照预期工作节点是否执行完全由注解上的条件表达式决定。5. 常见问题与排查思路5.1 节点方法没有执行现象控制台完全没有节点执行日志。可能原因工作流类没有被 Spring 管理也就是缺少Component注解。工作流类上没有Workflow注解引擎会直接抛出异常。节点方法没有加WorkflowNode注解。排查思路检查工作流类上是否有Component和Workflow。检查节点方法是否有WorkflowNode。检查测试类注入的是不是同一个 Bean。解决方案补全注解重新启动测试。5.2 节点总是被跳过现象某个节点明明应该执行但日志显示“条件不满足跳过”。可能原因上下文没有