SpringBoot 遇上状态机:简化复杂业务逻辑的利器
系统环境:
- JAVA JDK 版本: openjdk 17
- SpringBoot 版本: 3.3.2
示例地址:
参考地址:
一、什么状态机
状态机一般指有限状态机 (finite-state machine,FSM),又称有限状态自动机 (finite-state automaton,FSA),是一个抽象的数学模型,通过定义一系列有限的状态以及状态之间的转换规则,来模拟现实世界或抽象系统的动态行为。每个状态代表系统可能存在的条件或阶段,而状态间的转换则是由特定的事件 (或输入) 触发的。
例如,有一扇电动门状态机,有 关闭(closed)/打开(opened) 两种状态,当触发 开门(open door)/关门(close door) 事件后,就会触发状态机执行 状态转换(transition),然后验证是否满足 转换条件(transition condition),满足条件就执行状态转换,将状态转换为 打开(opened)/关闭(closed) 状态。两种转换规则如下:
- 状态转换规则①: 关闭状态(closed) —> 开门事件(open door) —> 打开状态(opened)
- 状态转换规则②: 打开状态(opened) —> 关门事件(close door) —> 关闭状态(closed)

二、为什么要使用状态机
在实际开发过程中,我们常常会碰到复杂的业务逻辑,如订单状态转换、支付状态转换、发货状态转换等。以往,大家通常会用 if/else 或者 switch 语句来处理这类逻辑。不过,一旦业务逻辑变复杂,代码里就会充斥大量的 if/else 或 switch 语句,这会让代码结构变得臃肿,维护起来十分困难。而且,每当新增一种状态或条件,就得添加更多的判断逻辑,这不仅容易出错,还会让代码变得晦涩难懂,无法清晰展现系统的状态和行为。
public static void main(String[] args) { String orderState = "已创建"; String event = "支付"; if ("已创建".equals(orderState)) if ("支付".equals(event)) orderState = "待发货"; if ("已创建".equals(orderState)) if ("取消".equals(event)) orderState = "已关闭"; // ......}与之相比,状态机提供了一种更具结构化、便于理解和维护的方式来管理系统的状态与行为。每个状态都对应着一种特定的情形,状态之间的转换由明确的事件触发。这种方式把复杂问题拆解成了更小、更好管理的部分。
举个订单状态流转的例子,我们可以定义”已创建”、“待发货”、“已发货”、“已收货”和”已关闭” 等几个状态。当收到支付成功这个事件时,订单就会从”已创建”状态转换为”待发货”状态。在复杂业务逻辑中,这种清晰明确的状态转换规则能极大提升代码的可读性和可维护性。
总而言之,使用状态机进行设计能显著提升代码的可读性和可维护性。它把复杂的条件判断拆分成明确的状态和转换,不仅让代码更易理解,也方便团队里的其他开发人员接手和维护。所以,在需要管理复杂状态逻辑的场景下,采用状态机是一种既优雅又高效的设计选择。
三、状态机的几个概念
在状态机中,有以下几个基本概念需要了解:
- 状态(State): 状态表示系统在某一时刻所处的特定情形或配置。
- 事件(Event)/输入(Input): 事件(也叫输入)是一种外部条件或信号,它能促使状态机从一个状态转变到另一个状态。
- 转换 (Transition): 转换指的是状态转换,用来描述当特定事件发生时,状态机怎样从一个状态切换到另一个状态。每次转换通常由某个特定事件触发,并且可能会伴随一个或多个动作。
- 条件(Condition): 条件是状态转换的前提,只有当特定条件得到满足,状态机才会进行状态转换。
- 动作(Action)/输出(Output): 动作是在状态转换发生时执行的操作。它可以是逻辑处理、计算、输出信息、更新状态等。
除了一些基本概念,状态机中还有一些与状态相关的概念:
- 现态(Initial State): 现态指的是状态机当前所处的状态。
- 次态(Next State): 次态是状态转换后的目标状态。
- 初态(Initial State): 初态是状态机启动时所处的第一个状态。
- 终态 (Final State): 终态是状态机完成任务或达成目标后的状态,不过,有些状态机可能没有明确的终态,会持续运行下去。
四、状态机应用场景
状态机的应用场景非常广泛,特别是处理复杂逻辑和状态管理的时候。下面是一些常见的应用场景:
- 信号控制: 以交通信号灯为例,它有红、黄、绿三种状态。信号灯会依据时间,或者像紧急车辆通行这类外部事件,来进行状态的切换。
- 订单状态流转: 拿商品订单来说,它会经历创建、支付、发货、收货等一系列状态。每个状态都有各自特定的操作和转换规则。
- 游戏开发: 在游戏里,角色的行为可以用状态机来管理。比如游戏角色会有行走、跳跃、攻击等不同状态。
- 工作流引擎: 在企业的业务流程管理中,状态机能够定义和跟踪工作项。就像审批流程,会有申请、审核、批准等不同状态。
- 用户权限管理: 在 Web 应用里,用户的登录状态 (已登录或未登录) 会影响其能执行的操作,比如评论、转发、收藏等。
在上述这些场景中,状态机把与状态相关的行为封装起来,让不同状态之间的切换结构更清晰,从而简化了复杂业务逻辑的实现,提升了代码的可维护性和可扩展性。
五、状态机的设计原则
状态机的设计原则旨在确保状态机既高效又易于理解和维护。以下是设计状态机时应遵循的一些关键原则:
- (1) 明确定义状态:
- 每个状态都应该有明确的含义和职责。
- 状态的数量应该保持在合理的范围内,避免过度细化。
- (2) 清晰的状态转换:
- 明确规定状态间的转换规则,即什么事件会导致状态从一个转变到另一个。
- 状态转换应当是确定性的,即给定相同的输入或事件,状态机总是会产生相同的结果。
- (3) 最小化状态数量:
- 尽量减少状态的数量,以降低系统的复杂性。
- 合并相似的状态,避免状态爆炸。
- (4) 简洁的事件触发器:
- 事件应当简单明了,最好是一次只做一件事。
- 避免复杂的事件触发逻辑,这有助于减少状态机的复杂度。
- (5) 初始状态与终止状态:
- 明确指定状态机的初始状态,这是状态机启动时所处的状态。
- 如果适用的话,定义一个或多个终止状态,表示状态机完成其任务或达到某种结束条件。
- (6) 错误处理与容错:
- 设计状态机时要考虑可能出现的异常情况,并规划如何处理这些错误。
- 提供适当的错误恢复机制,以便状态机能够从故障中恢复。
- (7) 模块化与解耦:
- 将状态机分解为较小的、独立的状态机或子状态机,以便管理和维护。
- 确保状态机内部的各个部分尽可能地解耦,这样可以更容易地进行扩展和修改。
- (8) 可视化和文档化:
- 使用状态图或其他可视化工具来表示状态机,便于团队成员理解和沟通。
- 为状态机编写详细的文档,包括每个状态的作用、可能的转换以及事件触发器。
- (9) 测试与验证:
- 对状态机进行全面的测试,确保所有的状态和转换都能正确无误地工作。
- 使用单元测试和集成测试来验证状态机的行为符合预期。
遵循这些设计原则可以帮助我们开发者构建出可靠、高效并且易于维护的状态机系统。
六、Java 枚举实现状态机示例
比如,现在要实现一个订单转换的状态机,其中包含的状态和事件如下:
- 事件: PAY(支付)、SHIP(发货)、RECEIVE(收货)、CANCEL(取消)
- 状态: CREATED(已创建)、PENDING_SHIPMENT(待发货)、SHIPPED(已发货)、RECEIVED(已收货)、CLOSED(已关闭)
这个状态转换流程如下图所示:

在 Java 中实现这个订单状态机,最常用的就是使用 枚举(Enum) 的方式,这种方式需要定义一个表示不同状态的 状态枚举,以及表示触发状态变化的 事件枚举。此外,在状态枚举中还需要定义每个状态下的 转换规则。下面是一个简单的示例,展示了如何使用枚举来实现这样一个状态机。
6.1 定义订单事件枚举类
首先,就是先创建一个用于表示订单事件的 OrderEvent 枚举类,其中包含 PAY(支付)、SHIP(发货)、RECEIVE(收货)、CANCEL(取消) 四个事件。代码如下:
/** * 订单事件枚举 * * @author mydlq */public enum OrderEvent { // 支付 PAY, // 发货 SHIP, // 收货 RECEIVE, // 取消 CANCEL}6.2 定义订单状态枚举类
然后,再创建一个用于表示订单状态的 OrderState 枚举类,其中包含 CREATED(已创建)、PENDING_SHIPMENT(待发货)、SHIPPED(已发货)、RECEIVED(已收货)、CLOSED(已关闭) 五个状态。代码如下:
/** * 订单状态枚举 * * @author mydlq */public enum OrderState { // --- 已创建 --- CREATED { @Override public OrderState next(OrderEvent event) { if (event == OrderEvent.PAY) { return PENDING_SHIPMENT; } if (event == OrderEvent.CANCEL) { return CLOSED; } return this; } }, // --- 待发货 --- PENDING_SHIPMENT { @Override public OrderState next(OrderEvent event) { if (event == OrderEvent.SHIP) { return SHIPPED; } if (event == OrderEvent.CANCEL) { return CLOSED; } return this; } }, // --- 已发货 --- SHIPPED { @Override public OrderState next(OrderEvent event) { if (event == OrderEvent.RECEIVE) { return RECEIVED; } if (event == OrderEvent.CANCEL) { return CLOSED; } return this; } }, // --- 已收货 --- RECEIVED { @Override public OrderState next(OrderEvent event) { return this; } }, // --- 已关闭 --- CLOSED { @Override public OrderState next(OrderEvent event) { return this; } };
/** * 获取下一个状态的抽象方法 * @param event 事件 * @return 下一个状态 */ public abstract OrderState next(OrderEvent event);
}6.3 测试状态机状态转换
最后,创建一个用于测试状态机状态转换流程的 StateMachineTest 类,该类执行后将会输出用于绘制 plantUML 图的数据,我们可以根据图中的转换流程来验证状态转换是否正确。代码如下:
/** * 订单状态机测试 * * @author mydlq */public class StateMachineTest {
public static void main(String[] args) { // 定义记录原状态和目标状态的变量,便于控制台中输出状态转换 OrderState sourceState; OrderState targetState;
// 输出plantUML状态转换图的开始标志 System.out.println("@startuml");
// (1) 验证状态转换: 已创建 -> (支付) -> 待发货 sourceState = OrderState.CREATED; targetState = sourceState.next(OrderEvent.PAY); System.out.println(sourceState + " --> " + targetState + " : " + OrderEvent.PAY);
// (2) 验证状态转换: 待发货 -> (发货) -> 已发货 sourceState = OrderState.PENDING_SHIPMENT; targetState = targetState.next(OrderEvent.SHIP); System.out.println(sourceState + " --> " + targetState + " : " + OrderEvent.SHIP);
// (3) 验证状态转换: 已发货 -> (收货) -> 已收货 sourceState = OrderState.SHIPPED; targetState = targetState.next(OrderEvent.RECEIVE); System.out.println(sourceState + " --> " + targetState + " : " + OrderEvent.RECEIVE);
// (4) 验证状态转换: 已创建 -> (取消) -> 已关闭 sourceState = OrderState.CREATED; targetState = sourceState.next(OrderEvent.CANCEL); System.out.println(sourceState + " --> " + targetState + " : " + OrderEvent.CANCEL);
// (5) 验证状态转换: 待发货 -> (取消) -> 已关闭 sourceState = OrderState.PENDING_SHIPMENT; targetState = targetState.next(OrderEvent.CANCEL); System.out.println(sourceState + " --> " + targetState + " : " + OrderEvent.CANCEL);
// (6) 验证状态转换: 已发货 -> (取消) -> 已关闭 sourceState = OrderState.SHIPPED; targetState = targetState.next(OrderEvent.CANCEL); System.out.println(sourceState + " --> " + targetState + " : " + OrderEvent.CANCEL);
// 输出plantUML状态转换图的结束标志 System.out.println("@enduml"); }
}执行该测试类后,控制台输出的用于绘制 plantUML 图的数据如下:
@startumlCREATED --> PENDING_SHIPMENT : PAYPENDING_SHIPMENT --> SHIPPED : SHIPSHIPPED --> RECEIVED : RECEIVECREATED --> CLOSED : CANCELPENDING_SHIPMENT --> CLOSED : CANCELSHIPPED --> CLOSED : CANCEL@enduml我们可以打开 plantuml 这个网址,然后将绘制 plantUML 的数据复制到输入框中,就可以看到如下所示的订单状态流转图:

通过图中的状态转换流程,可以确认代码中的状态转换规则是否正确。
七、常见开源的状态机框架
7.1 常见的开源框架
虽然使用枚举的方式实现状态机很简单直观,但随着业务场景的复杂度增加,这种简单的枚举状态机可能无法满足非线性的状态流转需求。在这种情况下,可以选择扩展现有的状态机实现,或者采用功能更为全面的开源状态机框架。
目前,有许多成熟的开源状态机框架可供选择,例如 Spring StateMachine、Squirrel StateMachine、Cola StateMachine 等。其中,Spring StateMachine 和 Squirrel StateMachine 框架提供了非常丰富的功能,如状态嵌套、状态子状态机等。然而,这些框架的全面性也带来了额外的复杂性,可能会让初次使用者感到难以掌握。
然而,对于大多数实际项目而言,其实并不需要这些高级特性,如状态嵌套、状态并行、子状态机等。在简单的项目中,这些特性通常是不必要的。此外,这些框架通常是有状态的,这意味着在多实例部署或多线程环境下使用时,需要特别注意线程安全问题。为了确保线程安全,开发者往往需要引入锁机制,而这可能会对性能产生一定的负面影响。
相比之下,Cola StateMachine 框架是阿里开源的一个无状态的状态机框架,其设计非常简洁,易于上手,因此对于大多数实际项目来说,它已经足够使用了。
7.2 三款框架简单对比
这里大致对 Spring StateMachine、Squirrel StateMachine、Cola StateMachine 三款框架进行对比,如下所示:
| 比较项 | Spring StateMachine | Squirrel StateMachine | Cola StateMachine |
|---|---|---|---|
| 项目背景 | Spring 生态中的状态机框架 | 开源的轻量级状态机框架 | 阿里开源的一种应用于领域驱动和分布式系统中的状态机框架 |
| 功能性 | 支持状态机、状态、转换等概念,支持持久化、动态配置、监听和调节等功能 | 支持状态机、状态、转换等概念,支持快速建模和初始状态自适应等功能 | 支持状态机、状态、转换等概念,支持分布式锁、事件驱动、高可用等功能 |
| 性能 | 内部实现基于状态模式,性能较好 | 内部实现基于状态模式,性能较好 | 内部实现基于状态模式,性能较好 |
| 线程安全性 | 线程不安全,内部使用锁机制来保证线程安全 | 线程不安全,每个请求都创建一个实例,不能多线程共享 | 线程安全,可多线程共享 |
| 优点 | 集成 Spring 生态系统、代码质量高,文档齐全 | 轻量,易于使用 | 分布式锁、事件驱动、高可用、可扩展 |
| 劣势 | 学习曲线较陡峭,较重,内部复杂度较高,StateMachine实例的创建比较重,线程不安全 | 功能较少,线程不安全 | 开发人员需要了解其它 Alibaba 的框架 |
| 学习曲线 | 较高,需要掌握 Spring 生态和状态机概念 | 适中,使用方式和其它状态机框架类似 | 适中,部分功能需要结合 Alibaba 其它开源框架使用 |
| 典型应用 | 工作流、电影订票、电信业务等业务场景 | 自动化测试、故障定位等场景 | 领域驱动设计、分布式事务等场景 |
| 社区活跃度 | 高,广泛应用且得到大量反馈 | 较高,受到国内知名公司的重视 | 相对较少,近年来才开始兴起 |
综合来看,三个状态机框架各有优势及适用场景。比如,Spring StateMachine 更适合用于复杂业务场景,尤其适用于那些已经采用 Spring 生态系统的应用程序;Squirrel StateMachine 更适用于需要快速建模和轻量级实现的场景;Cola StateMachine 更偏向于支持分布式系统和领域驱动设计的需求。所以,具体选择使用哪个开源状态机框架,则需要根据实际需求和业务场景来决定。
八、Cola StateMachine 框架
8.1 Cola StateMachine 框架简介
Cola StateMachine 是阿里开源微服务治理框架 COLA(Clean Object-Oriented & Layered Architecture) 中的一个组件,是一个轻量级的状态机管理框架,该框架采用了无状态设计,并支持与 Spring 集成,允许用户通过直观的流式接口来定义状态和事件,从而增强了业务流程管理的灵活性和可维护性。
关于作者对于 Cola StateMachine 框架的介绍与思想,可以阅读 “实现一个状态机引擎,教你看清DSL的本质” 这篇博文。
8.2 Cola StateMachine 框架特点
Cola StateMachine 框架中的特点:
- 轻量级: 它是一个轻量级的状态机管理组件,适合用于简单的有限状态场景。
- 无状态设计: 状态机本身不保存任何状态信息,所有的状态数据都存储在外部,这使得状态机可以在分布式环境中轻松扩展。
- Spring 集成: 支持与 Spring 框架集成,可以方便地在 Spring 应用程序中使用。
- Fluent Interface: 提供了流畅的接口设计,使定义状态和事件变得直观易懂,简化了状态转换逻辑的编写。
- 事件驱动: 支持事件驱动机制,当特定事件发生时可以触发状态的改变。
- 高性能: 由于其轻量级特性和无状态设计,非常适合处理高并发的状态转换场景。
- 易于维护: 通过清晰的状态和事件定义,使得业务逻辑更容易理解和维护。
8.3 Cola StateMachine 框架概念
Cola StateMachine 框架中定义了几个核心概念:
- State: 状态
- Event: 事件,状态由事件触发,引起变化
- Transition: 流转,表示从一个状态到另一个状态
- External Transition: 外部流转,两个不同状态之间的流转
- Internal Transition: 内部流转,同一个状态之间的流转
- Condition: 条件,表示是否允许到达某个状态
- Action: 动作,到达某个状态之后,可以做什么
- StateMachine: 状态机
8.4 Cola StateMachine 使用入门
(1) Maven 中引入 cola-component-statemachine 依赖
本人项目是使用 Maven 来管理依赖的,所以需要在 pom.xml 文件中添加以下依赖即可:
<dependency> <groupId>com.alibaba.cola</groupId> <artifactId>cola-component-statemachine</artifactId> <version>5.0.0</version></dependency>(2) 定义状态枚举
定义状态枚举,用于表示状态机中的状态。
public enum States { // 状态1 STATE_1, // 状态2 STATE_2, // 状态3 STATE_3, // 状态4 STATE_4}(3) 定义事件枚举
定义事件枚举,用于表示状态机中的事件。
public enum Events { // 事件1 EVENT_1, // 事件2 EVENT_2, // 事件3 EVENT_3}(4) 定义上下文对象
创建一个上下文对象,用于传递状态转换过程中需要的参数。比如,在订单状态机中,需要传递订单ID来实现一些业务处理,这里的订单实体对象其实就是状态机中的上下文对象。
@Datapublic class Order { /** 订单ID */ private Long orderId;}(5) 创建状态机转换条件类
创建一个状态转换条件,用于验证状态转换的条件是否满足。如果执行状态转换过程中,条件不满足,则不会进行状态转换。
public class TransitionCondition implements Condition<Order> {
@Override public boolean isSatisfied(Order context) { // 这里验证条件,订单ID不能为空 return context.getOrderId() != null; }
}注: 在当前示例中为了方便,只创建一个条件类,供全部状态转换规则中使用,而实际应当根据状态转换规则来决定创建条件类的数量。
(6) 创建状态机转换动作类
创建一个状态转换动作,用于在状态转换过程中执行一些业务操作。比如,可以在状态转换过程中,打印状态转换日志,更新订单状态,或者发送消息等。
public class TransitionAction implements Action<States, Events, Order> {
@Override public void execute(States from, States to, Events event, Order context) { // 打印日志 System.out.println("输出订单 ID=" + context.getOrderId() + " 的状态转换过程:" + " 原始状态:" + from + " 目标状态:" + to + " 事件:" + event); // 执行其它操作 // ...... }
}注: 在当前示例中为了方便,只创建一个动作类,供全部状态转换规则中使用,而实际应当根据状态转换规则来决定创建动作类的数量。
(7) 创建状态机和转换规则
创建一个自定义的状态机类,并配置状态转换规则。这里总共需要配置3种状态转换规则,如下:
- ① 状态转换规则1: STATE_1 -> 事件(EVENT_1) -> STATE_2
- ② 状态转换规则2: STATE_2 -> 事件(EVENT_2) -> STATE_2
- ③ 状态转换规则3: STATE_1|STATE_2 -> 事件(EVENT_3) -> STATE_3
具体代码如下:
public class MyStateMachine {
public static void initStateMachine() { // 创建状态转换条件和动作 Condition<Order> condition = new TransitionCondition(); Action<States, Events, Order> action = new TransitionAction();
// 创建状态机构建器 StateMachineBuilder<States, Events, Order> builder = StateMachineBuilderFactory.create(); // 配置状态转换规则(1) - 外部状态流转 builder.externalTransition() .from(States.STATE_1) .to(States.STATE_2) .on(Events.EVENT_1) .when(condition) .perform(action); // 配置状态转换规则(2) - 内部状态流转 builder.internalTransition() .within(States.STATE_2) .on(Events.EVENT_2) .when(condition) .perform(action); // 配置状态转换规则(3) - 外部状态流转,多个原始状态转换为单个目标状态 builder.externalTransitions() .fromAmong(States.STATE_1, States.STATE_2) .to(States.STATE_3) .on(Events.EVENT_3) .when(condition) .perform(action);
// 定义状态机ID,并构建状态机 String stateMachineId = "my-state-machine-id"; StateMachine<States, Events, Order> stateMachine = builder.build(stateMachineId);
// 输出绘制planUML图的数据 System.out.println(stateMachine.generatePlantUML()); }
}(8) 使用并测试状态机状态转换
创建一个测试类,来使用状态机,并且验证状态转换是否生效。方法中包含的步骤如下:
- ⑴ 创建自定义状态机(全局只需要初始化一次状态机即可)。
- ⑵ 根据状态机ID,调用状态机工厂类获取创建的状态机对象。
- ⑶ 创建订单上下文对象,并且设置订单ID。
- ⑷ 触发事件,然后获取转换后的目标状态,并验证目标状态是否符合预期。
public class StateMachineTest {
public static void main(String[] args) { // 够级自定义状态机(全局只需要初始化一次状态机即可) MyStateMachine.initStateMachine();
// 根据状态机ID获取状态机 String stateMachineId = "my-state-machine-id"; StateMachine<States, Events, Order> stateMachine = StateMachineFactory.get(stateMachineId);
// 定义订单上下文对象 Order order = new Order(1001L);
// 触发事件,然后获取转换后的目标状态,并判断目标状态是否符合预期 System.out.println("------------- 状态机状态转换测试 -------------"); // 测试状态转换(1): STATE_1 -> (EVENT_1) -> STATE_2 States targetState1 = stateMachine.fireEvent(States.STATE_1, Events.EVENT_1, order); System.out.print("测试状态转换(1)结果: "); System.out.println(targetState1 == States.STATE_2 ? "状态符合预期" : "状态转换不符合预期");
// 测试状态转换(2): STATE_2 -> (EVENT_2) -> STATE_2 States targetState2 = stateMachine.fireEvent(States.STATE_2, Events.EVENT_2, order); System.out.print("测试状态转换(2)结果: "); System.out.println(targetState2 == States.STATE_2 ? "状态符合预期" : "状态转换不符合预期");
// 测试状态转换(3): STATE_1 -> (EVENT_3) -> STATE_3 和 STATE_2 -> (EVENT_3) -> STATE_3 States targetState3 = stateMachine.fireEvent(States.STATE_1, Events.EVENT_3, order); States targetState4 = stateMachine.fireEvent(States.STATE_2, Events.EVENT_3, order); System.out.print("测试状态转换(3)结果: "); System.out.println(targetState3 == States.STATE_3 && targetState4 == States.STATE_3 ? "状态符合预期" : "状态转换不符合预期"); }
}测试类启动后,输入到控制台的内容如下:
@startumlSTATE_1 --> STATE_2 : EVENT_1STATE_2 --> STATE_2 : EVENT_2STATE_1 --> STATE_3 : EVENT_3STATE_2 --> STATE_3 : EVENT_3@enduml
------------- 状态机状态转换测试 -------------订单ID=1001状态转换过程: STATE_1 -> (EVENT_1) -> STATE_2测试状态转换(1)结果: 状态符合预期订单ID=1001状态转换过程: STATE_2 -> (EVENT_2) -> STATE_2测试状态转换(2)结果: 状态符合预期订单ID=1001状态转换过程: STATE_1 -> (EVENT_3) -> STATE_3订单ID=1001状态转换过程: STATE_2 -> (EVENT_3) -> STATE_3测试状态转换(3)结果: 状态符合预期其中 @startuml 到 @enduml 之间是绘制状态转移 planUML 图的数据,可以复制到 plantuml 中查看。之后输出的内容则是验证状态机状态转换是否生效,以及状态转换是否按照预期的状态转换规则进行。
九、SpringBoot 结合 Cola StateMachine 状态机示例
接下来给出一个 SpringBoot 结合 Cola StateMachine 实现订单状态机的示例。项目执行流程如下图所示:

这里包含的事件和状态如下:
- 事件: PAY(支付)、SHIP(发货)、RECEIVE(收货)、CANCEL(取消)
- 状态: CREATED(已创建)、PENDING_SHIPMENT(待发货)、SHIPPED(已发货)、RECEIVED(已收货)、CLOSED(已关闭)
9.1 创建 MySQL 订单表
因为示例中涉及到事务,所以这里创建一个订单表,用于存储订单状态信息。建表 SQL 语句如下:
CREATE TABLE `order`( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id', `order_id` bigint(20) NOT NULL COMMENT '订单ID', `order_state` enum('CREATED','PENDING_SHIPMENT','SHIPPED','RECEIVED','CANCELED','CLOSED') NOT NULL COMMENT '订单状态', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`)) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='订单表';9.2 Maven 引入相关依赖
在 Maven 配置文件 pom.xml 中添加 spring-boot 和 cola-component-statemachine 相关依赖:
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion>
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.2</version> </parent>
<groupId>club.mydlq</groupId> <artifactId>spring-boot-cola-statemachine-example</artifactId> <version>0.0.1</version> <name>spring-boot-cola-statemachine-example</name> <description>statemachine example</description>
<properties> <java.version>17</java.version> </properties>
<dependencies> <!--spring-boot--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--cola-component-statemachine--> <dependency> <groupId>com.alibaba.cola</groupId> <artifactId>cola-component-statemachine</artifactId> <version>5.0.0</version> </dependency> <!--mybatis--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <scope>runtime</scope> </dependency> <!--test--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
</project>9.3 配置数据库连接参数
在 SpringBoot 的 application.yml 文件中配置数据库连接信息:
spring: application: name: spring-boot-cola-statemachine-example ## 数据库配置 datasource: type: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/example hikari: pool-name: DatebookHikariCP minimum-idle: 5 maximum-pool-size: 15 max-lifetime: 1800000 connection-timeout: 30000 username: root password: 123456
## MyBatis配置,指定扫描的 Mapper 文件位置mybatis: mapper-locations: classpath:mapper/*.xml9.4 定义订单状态枚举
创建一个订单状态枚举类,代码如下:
import lombok.AllArgsConstructor;import lombok.Getter;
/** * 订单状态枚举 * * @author mydlq */@Getter@AllArgsConstructorpublic enum OrderState { // 已创建 CREATED("已创建"), // 待发货 PENDING_SHIPMENT("待发货"), // 已发货 SHIPPED("已发货"), // 已收货 RECEIVED("已收货"), // 已关闭 CLOSED("已关闭"), ;
private final String desc;
@Override public String toString() { return this.name() + "_" + desc; }}9.5 定义订单事件枚举
创建一个订单事件枚举类,代码如下:
import lombok.AllArgsConstructor;import lombok.Getter;
/** * 订单事件枚举 * * @author mydlq */@Getter@AllArgsConstructorpublic enum OrderEvent { // 支付 PAY("支付" ), // 发货 SHIP("发货" ), // 收货 RECEIVE("收货" ), // 取消 CANCEL("取消" ), ;
private final String desc;
@Override public String toString() { return this.name() + "_" + desc; }}9.6 创建订单信息实体类
创建一个订单信息实体类,用于存储订单状态信息。
import club.mydlq.example.enums.OrderState;import lombok.AllArgsConstructor;import lombok.Data;
/** * 订单信息实体类 * * @author mydlq */@Data@AllArgsConstructorpublic class Order { /** * 订单ID */ private Long orderId; /** * 订单状态 */ private OrderState orderState;}8.7 创建状态转换条件
创建状态转换条件,因为状态转换中要校验的条件大多数都不是一样的,所以这里总共需要创建4个状态转换条件类,分别为:
- ⑴ 待发货状态转换条件: 需要校验当前状态是否为
CREATED(已创建); - ⑵ 已发货状态转换条件: 需要校验当前状态是否为
PENDING_SHIPMENT(待发货); - ⑶ 已收货状态转换条件: 需要校验当前状态是否为
SHIPPED(已发货); - ⑷ 已关闭状态转换条件: 需要校验当前状态是否为
CREATED(已创建)、PENDING_SHIPMENT(待发货)、SHIPPED(已发货)之一;
(1) 待发货状态转换条件
import club.mydlq.example.enums.OrderState;import club.mydlq.example.model.Order;import com.alibaba.cola.statemachine.Condition;import org.springframework.stereotype.Component;
/** * 待发货状态转换条件 * (1) CREATED(已创建) --> PENDING_SHIPMENT(待发货) * * @author mydlq */@Componentpublic class PendingShipmentCondition implements Condition<Order> {
@Override public boolean isSatisfied(Order context) { return context != null && context.getOrderId() != null && context.getOrderState() == OrderState.CREATED; }
}(2) 已发货状态转换条件
import club.mydlq.example.enums.OrderState;import club.mydlq.example.model.Order;import com.alibaba.cola.statemachine.Condition;import org.springframework.stereotype.Component;
/** * 已发货状态转换条件 * (1) PENDING_SHIPMENT(待发货) --> SHIPPED(已发货) * * @author mydlq */@Componentpublic class ShippedCondition implements Condition<Order> {
@Override public boolean isSatisfied(Order context) { return context != null && context.getOrderId() != null && context.getOrderState() == OrderState.PENDING_SHIPMENT; }
}(3) 已收货状态转换条件
import club.mydlq.example.enums.OrderState;import club.mydlq.example.model.Order;import com.alibaba.cola.statemachine.Condition;import org.springframework.stereotype.Component;
/** * 已收货状态转换条件 * (1) SHIPPED(已发货) --> RECEIVED(已收货) * * @author mydlq */@Componentpublic class ReceivedCondition implements Condition<Order> {
@Override public boolean isSatisfied(Order context) { return context != null && context.getOrderId() != null && context.getOrderState() == OrderState.SHIPPED; }
}(4) 已关闭状态转换条件
import club.mydlq.example.enums.OrderState;import club.mydlq.example.model.Order;import com.alibaba.cola.statemachine.Condition;import org.springframework.stereotype.Component;
/** * 已关闭状态转换条件 * (1) CREATED(已创建) --> CLOSED(已关闭) * (2) PENDING_SHIPMENT(待发货) --> CLOSED(已关闭) * (3) SHIPPED(已发货) --> CLOSED(已关闭) * * @author mydlq */@Componentpublic class ClosedCondition implements Condition<Order> {
@Override public boolean isSatisfied(Order context) { return context != null && context.getOrderId() != null && (context.getOrderState() == OrderState.CREATED || context.getOrderState() == OrderState.PENDING_SHIPMENT || context.getOrderState() == OrderState.SHIPPED); }
}9.8 创建状态转换动作
创建状态转换动作,因为目前在动作中只有打印状态转换日志,以及更新数据库订单状态的操作,所以这里只创建1个状态转换动作类。代码如下:
import club.mydlq.example.enums.OrderEvent;import club.mydlq.example.enums.OrderState;import club.mydlq.example.mapper.OrderMapper;import club.mydlq.example.model.Order;import com.alibaba.cola.statemachine.Action;import jakarta.annotation.Resource;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;
/** * 订单状态转换动作 * * @author mydlq */@Slf4j@Componentpublic class OrderAction implements Action<OrderState, OrderEvent, Order> {
@Resource private OrderMapper orderMapper;
@Override public void execute(OrderState from, OrderState to, OrderEvent event, Order context) { // 打印日志 System.out.printf("订单ID:%s: %s -> (%s) -> %s %n", context.getOrderId(), from, event, to); // 更新订单状态 context.setOrderState(to); orderMapper.update(context); }
}9.9 构建状态机并配置状态转换规则
创建一个状态机配置类,用于构建状态机,并且配置状态转换规则。这里总共需要配置6种状态转换规则,如下:
- ① 状态转换规则1: CREATED(已创建) -> PAY(支付) -> PENDING_SHIPMENT(待发货)
- ② 状态转换规则2: PENDING_SHIPMENT(待发货) -> SHIP(发货) -> SHIPPED(已发货)
- ③ 状态转换规则3: SHIPPED(已发货) -> RECEIVE(收货) -> RECEIVED(已收货)
- ④ 状态转换规则4: CREATED(已创建)|PENDING_SHIPMENT(待发货)|SHIPPED(已发货) -> CANCEL(取消) -> CLOSED(已关闭)
状态机配置类的代码如下:
import club.mydlq.example.config.action.*;import club.mydlq.example.config.condition.ClosedCondition;import club.mydlq.example.config.condition.PendingShipmentCondition;import club.mydlq.example.config.condition.ReceivedCondition;import club.mydlq.example.config.condition.ShippedCondition;import club.mydlq.example.enums.OrderEvent;import club.mydlq.example.enums.OrderState;import club.mydlq.example.model.Order;import com.alibaba.cola.statemachine.StateMachine;import com.alibaba.cola.statemachine.builder.StateMachineBuilder;import com.alibaba.cola.statemachine.builder.StateMachineBuilderFactory;import jakarta.annotation.Resource;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;
/** * 订单状态机配置 * * @author mydlq */@Configurationpublic class OrderStateMachineConfig {
/** * 条件 */ @Resource(name = "pendingShipmentCondition") private PendingShipmentCondition pendingShipmentCondition; @Resource(name = "shippedCondition") private ShippedCondition shippedCondition; @Resource(name = "receivedCondition") private ReceivedCondition receivedCondition; @Resource(name = "closedCondition") private ClosedCondition closedCondition; /** * 动作 */ @Resource(name = "orderAction") private OrderAction orderAction;
/** * 状态机ID */ private static final String STATE_MACHINE_ID = "orderStateMachine";
@Bean("orderStateMachine") public StateMachine<OrderState, OrderEvent, Order> orderStateMachine() { // (1) 生成一个状态机builder StateMachineBuilder<OrderState, OrderEvent, Order> builder = StateMachineBuilderFactory.create();
// (2) 通过使用builder配置外部状态转换 // - ①状态转换规则1: CREATED(已创建) -> PAY(支付) -> PENDING_SHIPMENT(待发货) builder.externalTransition() .from(OrderState.CREATED).to(OrderState.PENDING_SHIPMENT).on(OrderEvent.PAY) .when(pendingShipmentCondition) .perform(orderAction); // - ②状态转换规则2: PENDING_SHIPMENT(待发货) -> SHIP(发货) -> SHIPPED(已发货) builder.externalTransition() .from(OrderState.PENDING_SHIPMENT).to(OrderState.SHIPPED).on(OrderEvent.SHIP) .when(shippedCondition) .perform(orderAction); // - ③状态转换规则3: SHIPPED(已发货) -> RECEIVE(收货) -> RECEIVED(已收货) builder.externalTransition() .from(OrderState.SHIPPED).to(OrderState.RECEIVED).on(OrderEvent.RECEIVE) .when(receivedCondition) .perform(orderAction); // - ④状态转换规则4: // CREATED(已创建) -> CANCEL(取消) -> CLOSED(已关闭) // PENDING_SHIPMENT(待发货) -> CANCEL(取消) -> CLOSED(已关闭) // SHIPPED(已发货) -> CANCEL(取消) -> CLOSED(已关闭) builder.externalTransitions() .fromAmong(OrderState.CREATED, OrderState.PENDING_SHIPMENT, OrderState.SHIPPED) .to(OrderState.CLOSED) .on(OrderEvent.CANCEL) .when(closedCondition) .perform(orderAction);
// (3) 构建状态机 StateMachine<OrderState, OrderEvent, Order> orderStateMachine = builder.build(STATE_MACHINE_ID);
// (4) 输出状态机转换流程plantUML System.out.println(orderStateMachine.generatePlantUML());
return orderStateMachine; }
}9.10 创建订单 Mapper 类
import club.mydlq.example.model.Order;import org.apache.ibatis.annotations.Mapper;
/** * 订单 Mapper * * @author mydlq */@Mapperpublic interface OrderMapper { /** * 根据ID查询订单信息 * * @param orderId 订单ID * @return 执行结果 */ Order selectByOrderId(Long orderId);
/** * 保存订单信息 * * @param order 订单信息 * @return 执行结果 */ int save(Order order);
/** * 更新订单信息 * * @param order 订单信息 * @return 执行结果 */ int update(Order order);}然后再 resources/mapper 目录中,创建对应的 MyBatis 的 xml 文件,内如如下:
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="club.mydlq.example.mapper.OrderMapper">
<resultMap id="BaseResultMap" type="club.mydlq.example.model.Order"> <result property="orderId" column="order_id"/> <result property="orderState" column="order_state"/> </resultMap>
<!--根据ID查询订单信息--> <select id="selectByOrderId" parameterType="java.lang.Long" resultMap="BaseResultMap"> SELECT order_id, order_state FROM `order` WHERE order_id = #{orderId} </select>
<!--保存订单信息--> <insert id="save" keyColumn="id" keyProperty="id" parameterType="club.mydlq.example.model.Order"> INSERT INTO `order`(order_id, order_state) VALUES (#{orderId}, #{orderState}) </insert>
<!--更新订单信息--> <update id="update" parameterType="club.mydlq.example.model.Order"> UPDATE `order` SET order_id = #{orderId}, order_state = #{orderState}, update_time = NOW() WHERE order_id = #{orderId} </update>
</mapper>9.11 创建订单ID生成工具类
这里创建一个用于生成 订单ID 的工具类,这个类会在后续创建订单的 Service 类中使用。
import java.time.Instant;import java.time.LocalDateTime;import java.time.format.DateTimeFormatter;import java.util.concurrent.atomic.AtomicInteger;
/** * 订单工具类 * * @author mydlq */public class OrderIdUtil { private OrderIdUtil() { }
/** * 定义一个原子整型变量用于维护同一秒内的订单序列号 */ private static final AtomicInteger SEQUENCE = new AtomicInteger(1); /** * 定义一个格式化日期的工具 */ private static final DateTimeFormatter SDF = DateTimeFormatter.ofPattern("yyyyMMdd");
/** * 生成订单ID * * @return 订单ID */ public static Long generateOrderId() { /* * 订单ID组成: 【日期+时间戳+同一秒内的单号】 * - 第1部分: 下单的日期+时间戳。比如 2024年8月13日,则表示 20240813 * - 第2部分: 下单的时间戳。比如 01:20:30,转换为时间戳后则表示 04830 * - 第3部分: 同一秒内下的第几单,并且认为不会超过10000单。比如在同一秒内第1001单,则表示第 1001 单 */
// 获取当前日期和时间 LocalDateTime now = LocalDateTime.now();
// 格式化年月日 String dateStr = now.format(SDF);
// 获取当前时间戳 long timestamp = Instant.now().toEpochMilli();
// 取时间戳的后五位 int timestampPart = (int) (timestamp % 100000);
// 同一秒内的序列号 int sequenceNumber = SEQUENCE.getAndIncrement();
// 认为每秒订单量不会超过10000,所以如果序列号超过9999,则说明已经到了下一秒,就需要重置序列号 if (sequenceNumber >= 10000) { // 重置序列号 SEQUENCE.set(0); sequenceNumber = SEQUENCE.getAndIncrement(); }
// 构造订单ID return Long.parseLong(dateStr + String.format("%05d", timestampPart) + String.format("%04d", sequenceNumber)); }
}9.12 创建订单 Service 类
这里需要创建一个订单的 Service 接口和实现类,其中,Service 接口定义了订单的创建、支付、发货、收货、取消等操作,实现类则实现了这些操作。
订单 Service 接口
/** * 订单 Service * * @author mydlq */public interface OrderService { /** * 创建订单 * @return 订单ID */ Long create();
/** * 支付 * @param orderId 订单ID */ void pay(Long orderId);
/** * 发货 * @param orderId 订单ID */ void ship(Long orderId);
/** * 确认收货 * @param orderId 订单ID */ void receive(Long orderId);
/** * 取消订单 * @param orderId 订单ID */ void cancel(Long orderId);}订单 Service 实现类
import club.mydlq.example.enums.OrderEvent;import club.mydlq.example.enums.OrderState;import club.mydlq.example.mapper.OrderMapper;import club.mydlq.example.model.Order;import club.mydlq.example.service.OrderService;import club.mydlq.example.utils.OrderIdUtil;import com.alibaba.cola.statemachine.StateMachine;import jakarta.annotation.Resource;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;
/** * 订单 Service * * @author mydlq */@Servicepublic class OrderServiceImpl implements OrderService { /** * 订单持久化 Mapper */ @Resource private OrderMapper orderMapper; /** * 订单状态机 */ @Resource(name = "orderStateMachine") StateMachine<OrderState, OrderEvent, Order> orderOperaMachine;
@Override @Transactional(rollbackFor = Exception.class) public Long create() { // 生成订单ID Long orderId = OrderIdUtil.generateOrderId(); // 创建订单并设置订单初始状态 Order order = new Order(orderId, OrderState.CREATED); // 执行订单创建的其它操作 // ... // 保存订单信息 orderMapper.save(order); return orderId; }
@Override @Transactional(rollbackFor = Exception.class) public void pay(Long orderId) { // 查询订单信息 Order order = this.queryOrder(orderId); // 触发状态机状态变更 OrderState targetState = orderOperaMachine.fireEvent(order.getOrderState(), OrderEvent.PAY, order); // 验证目标状态 if (targetState != OrderState.PENDING_SHIPMENT) { throw new RuntimeException("订单ID=" + orderId + "支付失败"); } // 执行订单支付的其它操作 // ...... }
@Override @Transactional(rollbackFor = Exception.class) public void ship(Long orderId) { // 查询订单信息 Order order = this.queryOrder(orderId); // 触发状态机状态变更 OrderState targetState = orderOperaMachine.fireEvent(order.getOrderState(), OrderEvent.SHIP, order); // 验证目标状态 if (targetState != OrderState.SHIPPED) { throw new RuntimeException("订单ID " + orderId + " 发货失败"); } // 执行订单发货的其它操作 // ...... }
@Override @Transactional(rollbackFor = Exception.class) public void receive(Long orderId) { // 查询订单信息 Order order = this.queryOrder(orderId); // 触发状态机状态变更 OrderState targetState = orderOperaMachine.fireEvent(order.getOrderState(), OrderEvent.RECEIVE, order); // 验证目标状态 if (targetState != OrderState.RECEIVED) { throw new RuntimeException("订单ID " + orderId + " 确认收货失败"); } // 执行确认收货的其它操作 // ...... }
@Override @Transactional(rollbackFor = Exception.class) public void cancel(Long orderId) { // 查询订单信息 Order order = this.queryOrder(orderId); // 触发状态机状态变更 OrderState targetState = orderOperaMachine.fireEvent(order.getOrderState(), OrderEvent.CANCEL, order); // 验证目标状态 if (targetState != OrderState.CLOSED) { throw new RuntimeException("订单ID " + orderId + " 取消订单失败: "); } // 执行取消订单的其它操作 // ...... }
/** * 查询订单信息 * * @param orderId 订单ID * @return 订单信息 */ private Order queryOrder(Long orderId) { // 查询订单信息 Order order = orderMapper.selectByOrderId(orderId); if (order == null) { throw new RuntimeException("订单ID " + orderId + " 的订单不存在"); } return order; }
}9.13 创建订单 Controller 类
创建一个订单 Controller 类,其中包含创建订单、支付、发货、收货、取消订单等操作的接口。
import club.mydlq.example.service.OrderService;import jakarta.annotation.Resource;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;
/** * 订单 Controller * * @author mydlq */@RestController@RequestMapping("/order")public class OrderController {
@Resource private OrderService orderService;
/** * 创建订单 */ @PostMapping("/create") public ResponseEntity<Long> createOrder() { Long orderId = orderService.create(); return ResponseEntity.ok(orderId); }
/** * 订单支付 */ @PostMapping("/pay") public void pay(@RequestParam Long orderId) { orderService.pay(orderId); }
/** * 订单发货 */ @PostMapping("/ship") public void ship(@RequestParam Long orderId) { orderService.ship(orderId); }
/** * 确认收货 */ @PostMapping("/receive") public void receive(@RequestParam Long orderId) { orderService.receive(orderId); }
/** * 取消订单 */ @PostMapping("/cancel") public void cancel(@RequestParam Long orderId) { orderService.cancel(orderId); }
}9.14 创建 SpringBoot 启动类
import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;
/** * 启动类 * * @author mydlq */@SpringBootApplicationpublic class Application {
public static void main(String[] args) { SpringApplication.run(Application.class, args); }
}9.15 启动项目输出 planUML 流程过程
在上面示例中,我们创建订单状态机配置时,在第 (4) 步骤中,设置状态机在被创建时,输出订单状态机转换的 plantUML 流程。所以,当 SpringBoot 项目启动后,就会在控制台输出如下的 plantuml 流程描述:
@startumlCREATED_已创建 --> PENDING_SHIPMENT_待发货 : PAY_支付PENDING_SHIPMENT_待发货 --> SHIPPED_已发货 : SHIP_发货SHIPPED_已发货 --> RECEIVED_已收货 : RECEIVE_收货CREATED_已创建 --> CLOSED_已关闭 : CANCEL_取消PENDING_SHIPMENT_待发货 --> CLOSED_已关闭 : CANCEL_取消SHIPPED_已发货 --> CLOSED_已关闭 : CANCEL_取消@enduml我们可以打开 plantuml 这个网址,然后将上面的内容输入到 plantuml 的输入框中,然后我们就可以看到如下所示的订单状态流转图:

在图中展示的订单状态转换,就是我们在应用中配置的订单状态转换规则,可以根据图中的流转方向来确认,配置的规则是否正确。
9.16 创建订单状态机测试类
接下来,我们创建一个测试类,用于验证当指定事件发生后,对应的订单状态流转是否正确。测试类代码如下:
import club.mydlq.example.Application;import org.junit.jupiter.api.MethodOrderer;import org.junit.jupiter.api.Order;import org.junit.jupiter.api.Test;import org.junit.jupiter.api.TestMethodOrder;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.web.servlet.MockMvc;import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/** * 订单状态转换测试 * * @author mydlq */@AutoConfigureMockMvc@SpringBootTest(classes = Application.class)@TestMethodOrder(MethodOrderer.OrderAnnotation.class)public class OrderControllerTest {
@Autowired private MockMvc mockMvc;
/** * 调用下单接口,获取一个订单ID */ public String createOrder() throws Exception { // 模拟调用下单接口,获取一个订单ID return mockMvc.perform(post("/order/create")) .andExpect(status().isOk()) .andReturn() .getResponse() .getContentAsString(); }
/** * (1) 测试状态转换: 已创建 -> (支付) -> 待发货 */ @Test @Order(1) public void testCreatedToPendingShipment() throws Exception { System.out.println("---(1) 测试状态转换: 已创建 -> (支付) -> 待发货---"); String orderId = createOrder(); //下单 mockMvc.perform(post("/order/pay").param("orderId", orderId)); //支付 }
/** * (2) 测试状态转换: 待发货 -> (发货) -> 已发货 */ @Test @Order(2) public void testPendingShipmentToShipped() throws Exception { System.out.println("\n---(2) 测试状态转换: 待发货 -> (发货) -> 已发货---" ); String orderId = createOrder(); //下单 mockMvc.perform(post("/order/pay").param("orderId", orderId)); //支付 mockMvc.perform(post("/order/ship").param("orderId", orderId)); //发货 }
/** * (3) 测试状态转换: 已发货 -> (收货) -> 已收货 */ @Test @Order(3) public void testShippedToReceived() throws Exception { System.out.println("\n---(3) 测试状态转换: 已发货 -> (收货) -> 已收货---"); String orderId = createOrder(); //下单 mockMvc.perform(post("/order/pay").param("orderId", orderId)); //支付 mockMvc.perform(post("/order/ship").param("orderId", orderId)); //发货 mockMvc.perform(post("/order/receive").param("orderId", orderId)); //收货 }
/** * (4) 测试状态转换: 已创建 -> (取消) -> 已取消 */ @Test @Order(4) public void testCreatedToCanceled() throws Exception { System.out.println("\n---(4) 测试状态转换: 已创建 -> (取消) -> 已关闭---"); String orderId = createOrder(); //下单 mockMvc.perform(post("/order/cancel").param("orderId", orderId)); //取消 }
/** * (5) 测试状态转换: 待发货 -> (取消) -> 已取消 */ @Test @Order(5) public void testPendingShipmentToCanceled() throws Exception { System.out.println("\n---(5) 测试状态转换: 待发货 -> (取消) -> 已关闭---"); String orderId = createOrder(); //下单 mockMvc.perform(post("/order/pay").param("orderId", orderId)); //支付 mockMvc.perform(post("/order/cancel").param("orderId", orderId)); //取消 }
/** * (6) 测试状态转换: 已发货 -> (取消) -> 已取消 */ @Test @Order(6) public void testShippedToCanceled() throws Exception { System.out.println("\n---(6) 测试状态转换: 已发货 -> (取消) -> 已关闭---"); String orderId = createOrder(); //下单 mockMvc.perform(post("/order/pay").param("orderId", orderId)); //支付 mockMvc.perform(post("/order/ship").param("orderId", orderId)); //发货 mockMvc.perform(post("/order/cancel").param("orderId", orderId)); //取消 }
}9.17 启动测试类进行验证
之后,启动测试类 OrderControllerTest,来验证配置的转换规则是否正确,启动后控制台输入内容如下:
---(1) 测试状态转换: 已创建 -> (支付) -> 待发货---订单ID:20240816182480001: CREATED_已创建 -> (PAY_支付) -> PENDING_SHIPMENT_待发货
---(2) 测试状态转换: 待发货 -> (发货) -> 已发货---订单ID:20240816184070002: CREATED_已创建 -> (PAY_支付) -> PENDING_SHIPMENT_待发货订单ID:20240816184070002: PENDING_SHIPMENT_待发货 -> (SHIP_发货) -> SHIPPED_已发货
---(3) 测试状态转换: 已发货 -> (收货) -> 已收货---订单ID:20240816184480003: CREATED_已创建 -> (PAY_支付) -> PENDING_SHIPMENT_待发货订单ID:20240816184480003: PENDING_SHIPMENT_待发货 -> (SHIP_发货) -> SHIPPED_已发货订单ID:20240816184480003: SHIPPED_已发货 -> (RECEIVE_收货) -> RECEIVED_已收货
---(4) 测试状态转换: 已创建 -> (取消) -> 已关闭---订单ID:20240816185000004: CREATED_已创建 -> (CANCEL_取消) -> CLOSED_已关闭
---(5) 测试状态转换: 待发货 -> (取消) -> 已关闭---订单ID:20240816185290005: CREATED_已创建 -> (PAY_支付) -> PENDING_SHIPMENT_待发货订单ID:20240816185290005: PENDING_SHIPMENT_待发货 -> (CANCEL_取消) -> CLOSED_已关闭
---(6) 测试状态转换: 已发货 -> (取消) -> 已关闭---订单ID:20240816185630006: CREATED_已创建 -> (PAY_支付) -> PENDING_SHIPMENT_待发货订单ID:20240816185630006: PENDING_SHIPMENT_待发货 -> (SHIP_发货) -> SHIPPED_已发货订单ID:20240816185630006: SHIPPED_已发货 -> (CANCEL_取消) -> CLOSED_已关闭可以看到,测试结果符合预期。
--- END ---
