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
语句,从而使代码结构变得臃肿且难以维护。而且,每增加一种新的状态或条件,都需要添加更多的判断逻辑,这不仅增加了出错的风险,也让代码变得难以阅读和理解,无法清晰地表达系统的状态和行为。
相比之下,状态机提供了一种更结构化、易于理解和维护的方式来管理系统的状态和行为。每个状态都代表了一个特定的情况,而从一个状态到另一个状态的转换则是由明确的事件触发。这种方法将问题分解成更小、更易管理的部分。
例如,在订单状态流转的例子中,我们可以定义 "已创建"、"待发货"、"已发货"、"已收货"、"已关闭" 等状态。当接收到支付成功的事件时,订单就会从 "已创建" 转换为 "待发货" 的状态。这种明确的状态转换规则,在复杂的业务逻辑中能极大地提高代码的可读性和可维护性。
总而言之,使用状态机进行设计可以显著提升代码的可读性和可维护性。它可以将复杂的条件判断拆分为明确的状态和转换,不仅使代码更容易理解,也使团队中的其他开发人员更容易接手和维护。因此,在需要管理复杂状态逻辑的场景中,采用状态机是一种更为优雅和高效的设计选择。
三、状态机的几个概念
在状态机中,有以下几个基本概念需要了解:
- 状态(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(取消)
四个事件。代码如下:
1/**
2 * 订单事件枚举
3 *
4 * @author mydlq
5 */
6public enum OrderEvent {
7 // 支付
8 PAY,
9 // 发货
10 SHIP,
11 // 收货
12 RECEIVE,
13 // 取消
14 CANCEL
15}
6.2 定义订单状态枚举类
然后,再创建一个用于表示订单状态的 OrderState
枚举类,其中包含 CREATED(已创建)
、PENDING_SHIPMENT(待发货)
、SHIPPED(已发货)
、RECEIVED(已收货)
、CLOSED(已关闭)
五个状态。代码如下:
1/**
2 * 订单状态枚举
3 *
4 * @author mydlq
5 */
6public enum OrderState {
7 // --- 已创建 ---
8 CREATED {
9 @Override
10 public OrderState next(OrderEvent event) {
11 if (event == OrderEvent.PAY) { return PENDING_SHIPMENT; }
12 if (event == OrderEvent.CANCEL) { return CLOSED; }
13 return this;
14 }
15 },
16 // --- 待发货 ---
17 PENDING_SHIPMENT {
18 @Override
19 public OrderState next(OrderEvent event) {
20 if (event == OrderEvent.SHIP) { return SHIPPED; }
21 if (event == OrderEvent.CANCEL) { return CLOSED; }
22 return this;
23 }
24 },
25 // --- 已发货 ---
26 SHIPPED {
27 @Override
28 public OrderState next(OrderEvent event) {
29 if (event == OrderEvent.RECEIVE) { return RECEIVED; }
30 return this;
31 }
32 },
33 // --- 已收货 ---
34 RECEIVED {
35 @Override
36 public OrderState next(OrderEvent event) { return this; }
37 },
38 // --- 已关闭 ---
39 CLOSED {
40 @Override
41 public OrderState next(OrderEvent event) { return this; }
42 };
43
44 /**
45 * 获取下一个状态的抽象方法
46 * @param event 事件
47 * @return 下一个状态
48 */
49 public abstract OrderState next(OrderEvent event);
50
51}
6.3 测试状态机状态转换
最后,创建一个用于测试状态机状态转换流程的 StateMachineTest
类,该类执行后将会输出用于绘制 plantUML
图的数据,我们可以根据图中的转换流程来验证状态转换是否正确。代码如下:
1/**
2 * 订单状态机测试
3 *
4 * @author mydlq
5 */
6public class StateMachineTest {
7
8 public static void main(String[] args) {
9 // 定义记录原状态和目标状态的变量,便于控制台中输出状态转换
10 OrderState sourceState;
11 OrderState targetState;
12
13 // 输出plantUML状态转换图的开始标志
14 System.out.println("@startuml");
15
16 // (1) 验证状态转换: 已创建 -> (支付) -> 待发货
17 sourceState = OrderState.CREATED;
18 targetState = sourceState.next(OrderEvent.PAY);
19 System.out.println(sourceState + " --> " + targetState + " : " + OrderEvent.PAY);
20
21 // (2) 验证状态转换: 待发货 -> (发货) -> 已发货
22 sourceState = OrderState.PENDING_SHIPMENT;
23 targetState = targetState.next(OrderEvent.SHIP);
24 System.out.println(sourceState + " --> " + targetState + " : " + OrderEvent.SHIP);
25
26 // (3) 验证状态转换: 已发货 -> (收货) -> 已收货
27 sourceState = OrderState.SHIPPED;
28 targetState = targetState.next(OrderEvent.RECEIVE);
29 System.out.println(sourceState + " --> " + targetState + " : " + OrderEvent.RECEIVE);
30
31 // (4) 验证状态转换: 已创建 -> (取消) -> 已关闭
32 sourceState = OrderState.CREATED;
33 targetState = sourceState.next(OrderEvent.CANCEL);
34 System.out.println(sourceState + " --> " + targetState + " : " + OrderEvent.CANCEL);
35
36 // (5) 验证状态转换: 待发货 -> (取消) -> 已关闭
37 sourceState = OrderState.PENDING_SHIPMENT;
38 targetState = targetState.next(OrderEvent.CANCEL);
39 System.out.println(sourceState + " --> " + targetState + " : " + OrderEvent.CANCEL);
40
41 // (6) 验证状态转换: 已发货 -> (取消) -> 已关闭
42 sourceState = OrderState.SHIPPED;
43 targetState = targetState.next(OrderEvent.CANCEL);
44 System.out.println(sourceState + " --> " + targetState + " : " + OrderEvent.CANCEL);
45
46 // 输出plantUML状态转换图的结束标志
47 System.out.println("@enduml");
48 }
49
50}
执行该测试类后,控制台输出的用于绘制 plantUML
图的数据如下:
1@startuml
2CREATED --> PENDING_SHIPMENT : PAY
3PENDING_SHIPMENT --> SHIPPED : SHIP
4SHIPPED --> RECEIVED : RECEIVE
5CREATED --> CLOSED : CANCEL
6PENDING_SHIPMENT --> CLOSED : CANCEL
7SHIPPED --> CLOSED : CANCEL
8@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) 引入 cola-component-statemachine 依赖
1<dependency>
2 <groupId>com.alibaba.cola</groupId>
3 <artifactId>cola-component-statemachine</artifactId>
4 <version>5.0.0</version>
5</dependency>
(2) 定义状态枚举
定义状态枚举,用于表示状态机中的状态。
1public enum States {
2 // 状态1
3 STATE_1,
4 // 状态2
5 STATE_2,
6 // 状态3
7 STATE_3,
8 // 状态4
9 STATE_4
10}
(3) 定义事件枚举
定义事件枚举,用于表示状态机中的事件。
1public enum Events {
2 // 事件1
3 EVENT_1,
4 // 事件2
5 EVENT_2,
6 // 事件3
7 EVENT_3
8}
(4) 定义上下文对象
创建一个上下文对象,用于传递状态转换过程中需要的参数。比如,在订单状态机中,需要传递订单ID来实现一些业务处理,这里的订单实体对象其实就是状态机中的上下文对象。
1@Data
2public class Order {
3 /** 订单ID */
4 private Long orderId;
5}
(5) 创建状态机转换条件类
创建一个状态转换条件,用于验证状态转换的条件是否满足。如果执行状态转换过程中,条件不满足,则不会进行状态转换。
1public class TransitionCondition implements Condition<Order> {
2
3 @Override
4 public boolean isSatisfied(Order context) {
5 // 这里验证条件,订单ID不能为空
6 return context.getOrderId() != null;
7 }
8
9}
注: 在当前示例中为了方便,只创建一个条件类,供全部状态转换规则中使用,而实际应当根据状态转换规则来决定创建条件类的数量。
(6) 创建状态机转换动作类
创建一个状态转换动作,用于在状态转换过程中执行一些业务操作。比如,可以在状态转换过程中,打印状态转换日志,更新订单状态,或者发送消息等。
1public class TransitionAction implements Action<States, Events, Order> {
2
3 @Override
4 public void execute(States from, States to, Events event, Order context) {
5 // 打印日志
6 System.out.println("输出订单 ID=" + context.getOrderId() + " 的状态转换过程:" +
7 " 原始状态:" + from + " 目标状态:" + to + " 事件:" + event);
8 // 执行其它操作
9 // ......
10 }
11
12}
注: 在当前示例中为了方便,只创建一个动作类,供全部状态转换规则中使用,而实际应当根据状态转换规则来决定创建动作类的数量。
(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
具体代码如下:
1public class MyStateMachine {
2
3 public static void initStateMachine() {
4 // 创建状态转换条件和动作
5 Condition<Order> condition = new TransitionCondition();
6 Action<States, Events, Order> action = new TransitionAction();
7
8 // 创建状态机构建器
9 StateMachineBuilder<States, Events, Order> builder = StateMachineBuilderFactory.create();
10 // 配置状态转换规则(1) - 外部状态流转
11 builder.externalTransition()
12 .from(States.STATE_1)
13 .to(States.STATE_2)
14 .on(Events.EVENT_1)
15 .when(condition)
16 .perform(action);
17 // 配置状态转换规则(2) - 内部状态流转
18 builder.internalTransition()
19 .within(States.STATE_2)
20 .on(Events.EVENT_2)
21 .when(condition)
22 .perform(action);
23 // 配置状态转换规则(3) - 外部状态流转,多个原始状态转换为单个目标状态
24 builder.externalTransitions()
25 .fromAmong(States.STATE_1, States.STATE_2)
26 .to(States.STATE_3)
27 .on(Events.EVENT_3)
28 .when(condition)
29 .perform(action);
30
31 // 定义状态机ID,并构建状态机
32 String stateMachineId = "my-state-machine-id";
33 StateMachine<States, Events, Order> stateMachine = builder.build(stateMachineId);
34
35 // 输出绘制planUML图的数据
36 System.out.println(stateMachine.generatePlantUML());
37 }
38
39}
(8) 使用并测试状态机状态转换
创建一个测试类,来使用状态机,并且验证状态转换是否生效。方法中包含的步骤如下:
- ⑴ 创建自定义状态机(全局只需要初始化一次状态机即可)。
- ⑵ 根据状态机ID,调用状态机工厂类获取创建的状态机对象。
- ⑶ 创建订单上下文对象,并且设置订单ID。
- ⑷ 触发事件,然后获取转换后的目标状态,并验证目标状态是否符合预期。
1public class StateMachineTest {
2
3 public static void main(String[] args) {
4 // 够级自定义状态机(全局只需要初始化一次状态机即可)
5 MyStateMachine.initStateMachine();
6
7 // 根据状态机ID获取状态机
8 String stateMachineId = "my-state-machine-id";
9 StateMachine<States, Events, Order> stateMachine = StateMachineFactory.get(stateMachineId);
10
11 // 定义订单上下文对象
12 Order order = new Order(1001L);
13
14 // 触发事件,然后获取转换后的目标状态,并判断目标状态是否符合预期
15 System.out.println("------------- 状态机状态转换测试 -------------");
16 // 测试状态转换(1): STATE_1 -> (EVENT_1) -> STATE_2
17 States targetState1 = stateMachine.fireEvent(States.STATE_1, Events.EVENT_1, order);
18 System.out.print("测试状态转换(1)结果: ");
19 System.out.println(targetState1 == States.STATE_2 ? "状态符合预期" : "状态转换不符合预期");
20
21 // 测试状态转换(2): STATE_2 -> (EVENT_2) -> STATE_2
22 States targetState2 = stateMachine.fireEvent(States.STATE_2, Events.EVENT_2, order);
23 System.out.print("测试状态转换(2)结果: ");
24 System.out.println(targetState2 == States.STATE_2 ? "状态符合预期" : "状态转换不符合预期");
25
26 // 测试状态转换(3): STATE_1 -> (EVENT_3) -> STATE_3 和 STATE_2 -> (EVENT_3) -> STATE_3
27 States targetState3 = stateMachine.fireEvent(States.STATE_1, Events.EVENT_3, order);
28 States targetState4 = stateMachine.fireEvent(States.STATE_2, Events.EVENT_3, order);
29 System.out.print("测试状态转换(3)结果: ");
30 System.out.println(targetState3 == States.STATE_3 && targetState4 == States.STATE_3 ?
31 "状态符合预期" : "状态转换不符合预期");
32 }
33
34}
测试类启动后,输入到控制台的内容如下:
1@startuml
2STATE_1 --> STATE_2 : EVENT_1
3STATE_2 --> STATE_2 : EVENT_2
4STATE_1 --> STATE_3 : EVENT_3
5STATE_2 --> STATE_3 : EVENT_3
6@enduml
7
8------------- 状态机状态转换测试 -------------
9订单ID=1001状态转换过程: STATE_1 -> (EVENT_1) -> STATE_2
10测试状态转换(1)结果: 状态符合预期
11订单ID=1001状态转换过程: STATE_2 -> (EVENT_2) -> STATE_2
12测试状态转换(2)结果: 状态符合预期
13订单ID=1001状态转换过程: STATE_1 -> (EVENT_3) -> STATE_3
14订单ID=1001状态转换过程: STATE_2 -> (EVENT_3) -> STATE_3
15测试状态转换(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 语句如下:
1CREATE TABLE `order`
2(
3 `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
4 `order_id` bigint(20) NOT NULL COMMENT '订单ID',
5 `order_state` enum('CREATED','PENDING_SHIPMENT','SHIPPED','RECEIVED','CANCELED','CLOSED') NOT NULL COMMENT '订单状态',
6 `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
7 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
8 PRIMARY KEY (`id`)
9) ENGINE = InnoDB
10 DEFAULT CHARSET = utf8 COMMENT ='订单表';
9.2 Maven 引入相关依赖
在 Maven 配置文件 pom.xml 中添加 spring-boot
和 cola-component-statemachine
相关依赖:
1<?xml version="1.0" encoding="UTF-8"?>
2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4 <modelVersion>4.0.0</modelVersion>
5
6 <parent>
7 <groupId>org.springframework.boot</groupId>
8 <artifactId>spring-boot-starter-parent</artifactId>
9 <version>3.3.2</version>
10 </parent>
11
12 <groupId>club.mydlq</groupId>
13 <artifactId>spring-boot-cola-statemachine-example</artifactId>
14 <version>0.0.1</version>
15 <name>spring-boot-cola-statemachine-example</name>
16 <description>statemachine example</description>
17
18 <properties>
19 <java.version>17</java.version>
20 </properties>
21
22 <dependencies>
23 <!--spring-boot-->
24 <dependency>
25 <groupId>org.springframework.boot</groupId>
26 <artifactId>spring-boot-starter-web</artifactId>
27 </dependency>
28 <!--cola-component-statemachine-->
29 <dependency>
30 <groupId>com.alibaba.cola</groupId>
31 <artifactId>cola-component-statemachine</artifactId>
32 <version>5.0.0</version>
33 </dependency>
34 <!--mybatis-->
35 <dependency>
36 <groupId>org.mybatis.spring.boot</groupId>
37 <artifactId>mybatis-spring-boot-starter</artifactId>
38 <version>3.0.3</version>
39 </dependency>
40 <!--mysql-->
41 <dependency>
42 <groupId>mysql</groupId>
43 <artifactId>mysql-connector-java</artifactId>
44 <version>8.0.33</version>
45 <scope>runtime</scope>
46 </dependency>
47 <!--test-->
48 <dependency>
49 <groupId>org.springframework.boot</groupId>
50 <artifactId>spring-boot-starter-test</artifactId>
51 <scope>test</scope>
52 </dependency>
53 <!--lombok-->
54 <dependency>
55 <groupId>org.projectlombok</groupId>
56 <artifactId>lombok</artifactId>
57 <optional>true</optional>
58 </dependency>
59 </dependencies>
60
61 <build>
62 <plugins>
63 <plugin>
64 <groupId>org.springframework.boot</groupId>
65 <artifactId>spring-boot-maven-plugin</artifactId>
66 </plugin>
67 </plugins>
68 </build>
69
70</project>
9.3 配置数据库连接参数
在 SpringBoot 的 application.yml
文件中配置数据库连接信息:
1spring:
2 application:
3 name: spring-boot-cola-statemachine-example
4 ## 数据库配置
5 datasource:
6 type: com.zaxxer.hikari.HikariDataSource
7 driverClassName: com.mysql.cj.jdbc.Driver
8 url: jdbc:mysql://127.0.0.1:3306/example
9 hikari:
10 pool-name: DatebookHikariCP
11 minimum-idle: 5
12 maximum-pool-size: 15
13 max-lifetime: 1800000
14 connection-timeout: 30000
15 username: root
16 password: 123456
17
18## MyBatis配置,指定扫描的 Mapper 文件位置
19mybatis:
20 mapper-locations: classpath:mapper/*.xml
9.4 定义订单状态枚举
创建一个订单状态枚举类,代码如下:
1import lombok.AllArgsConstructor;
2import lombok.Getter;
3
4/**
5 * 订单状态枚举
6 *
7 * @author mydlq
8 */
9@Getter
10@AllArgsConstructor
11public enum OrderState {
12 // 已创建
13 CREATED("已创建"),
14 // 待发货
15 PENDING_SHIPMENT("待发货"),
16 // 已发货
17 SHIPPED("已发货"),
18 // 已收货
19 RECEIVED("已收货"),
20 // 已关闭
21 CLOSED("已关闭"),
22 ;
23
24 private final String desc;
25
26 @Override
27 public String toString() {
28 return this.name() + "_" + desc;
29 }
30}
9.5 定义订单事件枚举
创建一个订单事件枚举类,代码如下:
1import lombok.AllArgsConstructor;
2import lombok.Getter;
3
4/**
5 * 订单事件枚举
6 *
7 * @author mydlq
8 */
9@Getter
10@AllArgsConstructor
11public enum OrderEvent {
12 // 支付
13 PAY("支付" ),
14 // 发货
15 SHIP("发货" ),
16 // 收货
17 RECEIVE("收货" ),
18 // 取消
19 CANCEL("取消" ),
20 ;
21
22 private final String desc;
23
24 @Override
25 public String toString() {
26 return this.name() + "_" + desc;
27 }
28}
9.6 创建订单信息实体类
创建一个订单信息实体类,用于存储订单状态信息。
1import club.mydlq.example.enums.OrderState;
2import lombok.AllArgsConstructor;
3import lombok.Data;
4
5/**
6 * 订单信息实体类
7 *
8 * @author mydlq
9 */
10@Data
11@AllArgsConstructor
12public class Order {
13 /**
14 * 订单ID
15 */
16 private Long orderId;
17 /**
18 * 订单状态
19 */
20 private OrderState orderState;
21}
8.7 创建状态转换条件
创建状态转换条件,因为状态转换中要校验的条件大多数都不是一样的,所以这里总共需要创建4个状态转换条件类,分别为:
- ⑴ 待发货状态转换条件: 需要校验当前状态是否为
CREATED(已创建)
; - ⑵ 已发货状态转换条件: 需要校验当前状态是否为
PENDING_SHIPMENT(待发货)
; - ⑶ 已收货状态转换条件: 需要校验当前状态是否为
SHIPPED(已发货)
; - ⑷ 已关闭状态转换条件: 需要校验当前状态是否为
CREATED(已创建)
、PENDING_SHIPMENT(待发货)
、SHIPPED(已发货)
之一;
(1) 待发货状态转换条件
1import club.mydlq.example.enums.OrderState;
2import club.mydlq.example.model.Order;
3import com.alibaba.cola.statemachine.Condition;
4import org.springframework.stereotype.Component;
5
6/**
7 * 待发货状态转换条件
8 * (1) CREATED(已创建) --> PENDING_SHIPMENT(待发货)
9 *
10 * @author mydlq
11 */
12@Component
13public class PendingShipmentCondition implements Condition<Order> {
14
15 @Override
16 public boolean isSatisfied(Order context) {
17 return context != null
18 && context.getOrderId() != null
19 && context.getOrderState() == OrderState.CREATED;
20 }
21
22}
(2) 已发货状态转换条件
1import club.mydlq.example.enums.OrderState;
2import club.mydlq.example.model.Order;
3import com.alibaba.cola.statemachine.Condition;
4import org.springframework.stereotype.Component;
5
6/**
7 * 已发货状态转换条件
8 * (1) PENDING_SHIPMENT(待发货) --> SHIPPED(已发货)
9 *
10 * @author mydlq
11 */
12@Component
13public class ShippedCondition implements Condition<Order> {
14
15 @Override
16 public boolean isSatisfied(Order context) {
17 return context != null
18 && context.getOrderId() != null
19 && context.getOrderState() == OrderState.PENDING_SHIPMENT;
20 }
21
22}
(3) 已收货状态转换条件
1import club.mydlq.example.enums.OrderState;
2import club.mydlq.example.model.Order;
3import com.alibaba.cola.statemachine.Condition;
4import org.springframework.stereotype.Component;
5
6/**
7 * 已收货状态转换条件
8 * (1) SHIPPED(已发货) --> RECEIVED(已收货)
9 *
10 * @author mydlq
11 */
12@Component
13public class ReceivedCondition implements Condition<Order> {
14
15 @Override
16 public boolean isSatisfied(Order context) {
17 return context != null
18 && context.getOrderId() != null
19 && context.getOrderState() == OrderState.SHIPPED;
20 }
21
22}
(4) 已关闭状态转换条件
1import club.mydlq.example.enums.OrderState;
2import club.mydlq.example.model.Order;
3import com.alibaba.cola.statemachine.Condition;
4import org.springframework.stereotype.Component;
5
6/**
7 * 已关闭状态转换条件
8 * (1) CREATED(已创建) --> CLOSED(已关闭)
9 * (2) PENDING_SHIPMENT(待发货) --> CLOSED(已关闭)
10 * (3) SHIPPED(已发货) --> CLOSED(已关闭)
11 *
12 * @author mydlq
13 */
14@Component
15public class ClosedCondition implements Condition<Order> {
16
17 @Override
18 public boolean isSatisfied(Order context) {
19 return context != null
20 && context.getOrderId() != null
21 && (context.getOrderState() == OrderState.CREATED
22 || context.getOrderState() == OrderState.PENDING_SHIPMENT
23 || context.getOrderState() == OrderState.SHIPPED);
24 }
25
26}
9.8 创建状态转换动作
创建状态转换动作,因为目前在动作中只有打印状态转换日志,以及更新数据库订单状态的操作,所以这里只创建1个状态转换动作类。代码如下:
1import club.mydlq.example.enums.OrderEvent;
2import club.mydlq.example.enums.OrderState;
3import club.mydlq.example.mapper.OrderMapper;
4import club.mydlq.example.model.Order;
5import com.alibaba.cola.statemachine.Action;
6import jakarta.annotation.Resource;
7import lombok.extern.slf4j.Slf4j;
8import org.springframework.stereotype.Component;
9
10/**
11 * 订单状态转换动作
12 *
13 * @author mydlq
14 */
15@Slf4j
16@Component
17public class OrderAction implements Action<OrderState, OrderEvent, Order> {
18
19 @Resource
20 private OrderMapper orderMapper;
21
22 @Override
23 public void execute(OrderState from, OrderState to, OrderEvent event, Order context) {
24 // 打印日志
25 System.out.printf("订单ID:%s: %s -> (%s) -> %s %n", context.getOrderId(), from, event, to);
26 // 更新订单状态
27 context.setOrderState(to);
28 orderMapper.update(context);
29 }
30
31}
9.9 构建状态机并配置状态转换规则
创建一个状态机配置类,用于构建状态机,并且配置状态转换规则。这里总共需要配置6种状态转换规则,如下:
- ① 状态转换规则1: CREATED(已创建) -> PAY(支付) -> PENDING_SHIPMENT(待发货)
- ② 状态转换规则2: PENDING_SHIPMENT(待发货) -> SHIP(发货) -> SHIPPED(已发货)
- ③ 状态转换规则3: SHIPPED(已发货) -> RECEIVE(收货) -> RECEIVED(已收货)
- ④ 状态转换规则4: CREATED(已创建)|PENDING_SHIPMENT(待发货)|SHIPPED(已发货) -> CANCEL(取消) -> CLOSED(已关闭)
状态机配置类的代码如下:
1import club.mydlq.example.config.action.*;
2import club.mydlq.example.config.condition.ClosedCondition;
3import club.mydlq.example.config.condition.PendingShipmentCondition;
4import club.mydlq.example.config.condition.ReceivedCondition;
5import club.mydlq.example.config.condition.ShippedCondition;
6import club.mydlq.example.enums.OrderEvent;
7import club.mydlq.example.enums.OrderState;
8import club.mydlq.example.model.Order;
9import com.alibaba.cola.statemachine.StateMachine;
10import com.alibaba.cola.statemachine.builder.StateMachineBuilder;
11import com.alibaba.cola.statemachine.builder.StateMachineBuilderFactory;
12import jakarta.annotation.Resource;
13import org.springframework.context.annotation.Bean;
14import org.springframework.context.annotation.Configuration;
15
16/**
17 * 订单状态机配置
18 *
19 * @author mydlq
20 */
21@Configuration
22public class OrderStateMachineConfig {
23
24 /**
25 * 条件
26 */
27 @Resource(name = "pendingShipmentCondition")
28 private PendingShipmentCondition pendingShipmentCondition;
29 @Resource(name = "shippedCondition")
30 private ShippedCondition shippedCondition;
31 @Resource(name = "receivedCondition")
32 private ReceivedCondition receivedCondition;
33 @Resource(name = "closedCondition")
34 private ClosedCondition closedCondition;
35 /**
36 * 动作
37 */
38 @Resource(name = "orderAction")
39 private OrderAction orderAction;
40
41 /**
42 * 状态机ID
43 */
44 private static final String STATE_MACHINE_ID = "orderStateMachine";
45
46 @Bean("orderStateMachine")
47 public StateMachine<OrderState, OrderEvent, Order> orderStateMachine() {
48 // (1) 生成一个状态机builder
49 StateMachineBuilder<OrderState, OrderEvent, Order> builder = StateMachineBuilderFactory.create();
50
51 // (2) 通过使用builder配置外部状态转换
52 // - ①状态转换规则1: CREATED(已创建) -> PAY(支付) -> PENDING_SHIPMENT(待发货)
53 builder.externalTransition()
54 .from(OrderState.CREATED).to(OrderState.PENDING_SHIPMENT).on(OrderEvent.PAY)
55 .when(pendingShipmentCondition)
56 .perform(orderAction);
57 // - ②状态转换规则2: PENDING_SHIPMENT(待发货) -> SHIP(发货) -> SHIPPED(已发货)
58 builder.externalTransition()
59 .from(OrderState.PENDING_SHIPMENT).to(OrderState.SHIPPED).on(OrderEvent.SHIP)
60 .when(shippedCondition)
61 .perform(orderAction);
62 // - ③状态转换规则3: SHIPPED(已发货) -> RECEIVE(收货) -> RECEIVED(已收货)
63 builder.externalTransition()
64 .from(OrderState.SHIPPED).to(OrderState.RECEIVED).on(OrderEvent.RECEIVE)
65 .when(receivedCondition)
66 .perform(orderAction);
67 // - ④状态转换规则4:
68 // CREATED(已创建) -> CANCEL(取消) -> CLOSED(已关闭)
69 // PENDING_SHIPMENT(待发货) -> CANCEL(取消) -> CLOSED(已关闭)
70 // SHIPPED(已发货) -> CANCEL(取消) -> CLOSED(已关闭)
71 builder.externalTransitions()
72 .fromAmong(OrderState.CREATED,
73 OrderState.PENDING_SHIPMENT,
74 OrderState.SHIPPED)
75 .to(OrderState.CLOSED)
76 .on(OrderEvent.CANCEL)
77 .when(closedCondition)
78 .perform(orderAction);
79
80 // (3) 构建状态机
81 StateMachine<OrderState, OrderEvent, Order> orderStateMachine = builder.build(STATE_MACHINE_ID);
82
83 // (4) 输出状态机转换流程plantUML
84 System.out.println(orderStateMachine.generatePlantUML());
85
86 return orderStateMachine;
87 }
88
89}
9.10 创建订单 Mapper 类
1import club.mydlq.example.model.Order;
2import org.apache.ibatis.annotations.Mapper;
3
4/**
5 * 订单 Mapper
6 *
7 * @author mydlq
8 */
9@Mapper
10public interface OrderMapper {
11 /**
12 * 根据ID查询订单信息
13 *
14 * @param orderId 订单ID
15 * @return 执行结果
16 */
17 Order selectByOrderId(Long orderId);
18
19 /**
20 * 保存订单信息
21 *
22 * @param order 订单信息
23 * @return 执行结果
24 */
25 int save(Order order);
26
27 /**
28 * 更新订单信息
29 *
30 * @param order 订单信息
31 * @return 执行结果
32 */
33 int update(Order order);
34}
然后再 resources/mapper
目录中,创建对应的 MyBatis
的 xml
文件,内如如下:
1<?xml version="1.0" encoding="UTF-8"?>
2<!DOCTYPE mapper
3 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
4<mapper namespace="club.mydlq.example.mapper.OrderMapper">
5
6 <resultMap id="BaseResultMap" type="club.mydlq.example.model.Order">
7 <result property="orderId" column="order_id"/>
8 <result property="orderState" column="order_state"/>
9 </resultMap>
10
11 <!--根据ID查询订单信息-->
12 <select id="selectByOrderId" parameterType="java.lang.Long" resultMap="BaseResultMap">
13 SELECT order_id, order_state
14 FROM `order`
15 WHERE order_id = #{orderId}
16 </select>
17
18 <!--保存订单信息-->
19 <insert id="save" keyColumn="id" keyProperty="id" parameterType="club.mydlq.example.model.Order">
20 INSERT INTO `order`(order_id, order_state)
21 VALUES (#{orderId}, #{orderState})
22 </insert>
23
24 <!--更新订单信息-->
25 <update id="update" parameterType="club.mydlq.example.model.Order">
26 UPDATE `order`
27 SET order_id = #{orderId},
28 order_state = #{orderState},
29 update_time = NOW()
30 WHERE order_id = #{orderId}
31 </update>
32
33</mapper>
9.11 创建订单ID生成工具类
这里创建一个用于生成 订单ID
的工具类,这个类会在后续创建订单的 Service
类中使用。
1import java.time.Instant;
2import java.time.LocalDateTime;
3import java.time.format.DateTimeFormatter;
4import java.util.concurrent.atomic.AtomicInteger;
5
6/**
7 * 订单工具类
8 *
9 * @author mydlq
10 */
11public class OrderIdUtil {
12 private OrderIdUtil() {
13 }
14
15 /**
16 * 定义一个原子整型变量用于维护同一秒内的订单序列号
17 */
18 private static final AtomicInteger SEQUENCE = new AtomicInteger(1);
19 /**
20 * 定义一个格式化日期的工具
21 */
22 private static final DateTimeFormatter SDF = DateTimeFormatter.ofPattern("yyyyMMdd");
23
24 /**
25 * 生成订单ID
26 *
27 * @return 订单ID
28 */
29 public static Long generateOrderId() {
30 /*
31 * 订单ID组成: 【日期+时间戳+同一秒内的单号】
32 * - 第1部分: 下单的日期+时间戳。比如 2024年8月13日,则表示 20240813
33 * - 第2部分: 下单的时间戳。比如 01:20:30,转换为时间戳后则表示 04830
34 * - 第3部分: 同一秒内下的第几单,并且认为不会超过10000单。比如在同一秒内第1001单,则表示第 1001 单
35 */
36
37 // 获取当前日期和时间
38 LocalDateTime now = LocalDateTime.now();
39
40 // 格式化年月日
41 String dateStr = now.format(SDF);
42
43 // 获取当前时间戳
44 long timestamp = Instant.now().toEpochMilli();
45
46 // 取时间戳的后五位
47 int timestampPart = (int) (timestamp % 100000);
48
49 // 同一秒内的序列号
50 int sequenceNumber = SEQUENCE.getAndIncrement();
51
52 // 认为每秒订单量不会超过10000,所以如果序列号超过9999,则说明已经到了下一秒,就需要重置序列号
53 if (sequenceNumber >= 10000) {
54 // 重置序列号
55 SEQUENCE.set(0);
56 sequenceNumber = SEQUENCE.getAndIncrement();
57 }
58
59 // 构造订单ID
60 return Long.parseLong(dateStr +
61 String.format("%05d", timestampPart) +
62 String.format("%04d", sequenceNumber));
63 }
64
65}
9.12 创建订单 Service 类
这里需要创建一个订单的 Service 接口和实现类,其中,Service 接口定义了订单的创建、支付、发货、收货、取消等操作,实现类则实现了这些操作。
订单 Service 接口
1/**
2 * 订单 Service
3 *
4 * @author mydlq
5 */
6public interface OrderService {
7 /**
8 * 创建订单
9 * @return 订单ID
10 */
11 Long create();
12
13 /**
14 * 支付
15 * @param orderId 订单ID
16 */
17 void pay(Long orderId);
18
19 /**
20 * 发货
21 * @param orderId 订单ID
22 */
23 void ship(Long orderId);
24
25 /**
26 * 确认收货
27 * @param orderId 订单ID
28 */
29 void receive(Long orderId);
30
31 /**
32 * 取消订单
33 * @param orderId 订单ID
34 */
35 void cancel(Long orderId);
36}
订单 Service 实现类
1import club.mydlq.example.enums.OrderEvent;
2import club.mydlq.example.enums.OrderState;
3import club.mydlq.example.mapper.OrderMapper;
4import club.mydlq.example.model.Order;
5import club.mydlq.example.service.OrderService;
6import club.mydlq.example.utils.OrderIdUtil;
7import com.alibaba.cola.statemachine.StateMachine;
8import jakarta.annotation.Resource;
9import org.springframework.stereotype.Service;
10import org.springframework.transaction.annotation.Transactional;
11
12/**
13 * 订单 Service
14 *
15 * @author mydlq
16 */
17@Service
18public class OrderServiceImpl implements OrderService {
19 /**
20 * 订单持久化 Mapper
21 */
22 @Resource
23 private OrderMapper orderMapper;
24 /**
25 * 订单状态机
26 */
27 @Resource(name = "orderStateMachine")
28 StateMachine<OrderState, OrderEvent, Order> orderOperaMachine;
29
30 @Override
31 @Transactional(rollbackFor = Exception.class)
32 public Long create() {
33 // 生成订单ID
34 Long orderId = OrderIdUtil.generateOrderId();
35 // 创建订单并设置订单初始状态
36 Order order = new Order(orderId, OrderState.CREATED);
37 // 执行订单创建的其它操作
38 // ...
39 // 保存订单信息
40 orderMapper.save(order);
41 return orderId;
42 }
43
44 @Override
45 @Transactional(rollbackFor = Exception.class)
46 public void pay(Long orderId) {
47 // 查询订单信息
48 Order order = this.queryOrder(orderId);
49 // 触发状态机状态变更
50 OrderState targetState = orderOperaMachine.fireEvent(order.getOrderState(), OrderEvent.PAY, order);
51 // 验证目标状态
52 if (targetState != OrderState.PENDING_SHIPMENT) {
53 throw new RuntimeException("订单ID=" + orderId + "支付失败");
54 }
55 // 执行订单支付的其它操作
56 // ......
57 }
58
59 @Override
60 @Transactional(rollbackFor = Exception.class)
61 public void ship(Long orderId) {
62 // 查询订单信息
63 Order order = this.queryOrder(orderId);
64 // 触发状态机状态变更
65 OrderState targetState = orderOperaMachine.fireEvent(order.getOrderState(), OrderEvent.SHIP, order);
66 // 验证目标状态
67 if (targetState != OrderState.SHIPPED) {
68 throw new RuntimeException("订单ID " + orderId + " 发货失败");
69 }
70 // 执行订单发货的其它操作
71 // ......
72 }
73
74 @Override
75 @Transactional(rollbackFor = Exception.class)
76 public void receive(Long orderId) {
77 // 查询订单信息
78 Order order = this.queryOrder(orderId);
79 // 触发状态机状态变更
80 OrderState targetState = orderOperaMachine.fireEvent(order.getOrderState(), OrderEvent.RECEIVE, order);
81 // 验证目标状态
82 if (targetState != OrderState.RECEIVED) {
83 throw new RuntimeException("订单ID " + orderId + " 确认收货失败");
84 }
85 // 执行确认收货的其它操作
86 // ......
87 }
88
89 @Override
90 @Transactional(rollbackFor = Exception.class)
91 public void cancel(Long orderId) {
92 // 查询订单信息
93 Order order = this.queryOrder(orderId);
94 // 触发状态机状态变更
95 OrderState targetState = orderOperaMachine.fireEvent(order.getOrderState(), OrderEvent.CANCEL, order);
96 // 验证目标状态
97 if (targetState != OrderState.CLOSED) {
98 throw new RuntimeException("订单ID " + orderId + " 取消订单失败: ");
99 }
100 // 执行取消订单的其它操作
101 // ......
102 }
103
104 /**
105 * 查询订单信息
106 *
107 * @param orderId 订单ID
108 * @return 订单信息
109 */
110 private Order queryOrder(Long orderId) {
111 // 查询订单信息
112 Order order = orderMapper.selectByOrderId(orderId);
113 if (order == null) {
114 throw new RuntimeException("订单ID " + orderId + " 的订单不存在");
115 }
116 return order;
117 }
118
119}
9.13 创建订单 Controller 类
创建一个订单 Controller 类,其中包含创建订单、支付、发货、收货、取消订单等操作的接口。
1import club.mydlq.example.service.OrderService;
2import jakarta.annotation.Resource;
3import org.springframework.http.ResponseEntity;
4import org.springframework.web.bind.annotation.PostMapping;
5import org.springframework.web.bind.annotation.RequestMapping;
6import org.springframework.web.bind.annotation.RequestParam;
7import org.springframework.web.bind.annotation.RestController;
8
9/**
10 * 订单 Controller
11 *
12 * @author mydlq
13 */
14@RestController
15@RequestMapping("/order")
16public class OrderController {
17
18 @Resource
19 private OrderService orderService;
20
21 /**
22 * 创建订单
23 */
24 @PostMapping("/create")
25 public ResponseEntity<Long> createOrder() {
26 Long orderId = orderService.create();
27 return ResponseEntity.ok(orderId);
28 }
29
30 /**
31 * 订单支付
32 */
33 @PostMapping("/pay")
34 public void pay(@RequestParam Long orderId) {
35 orderService.pay(orderId);
36 }
37
38 /**
39 * 订单发货
40 */
41 @PostMapping("/ship")
42 public void ship(@RequestParam Long orderId) {
43 orderService.ship(orderId);
44 }
45
46 /**
47 * 确认收货
48 */
49 @PostMapping("/receive")
50 public void receive(@RequestParam Long orderId) {
51 orderService.receive(orderId);
52 }
53
54 /**
55 * 取消订单
56 */
57 @PostMapping("/cancel")
58 public void cancel(@RequestParam Long orderId) {
59 orderService.cancel(orderId);
60 }
61
62}
9.14 创建 SpringBoot 启动类
1import org.springframework.boot.SpringApplication;
2import org.springframework.boot.autoconfigure.SpringBootApplication;
3
4/**
5 * 启动类
6 *
7 * @author mydlq
8 */
9@SpringBootApplication
10public class Application {
11
12 public static void main(String[] args) {
13 SpringApplication.run(Application.class, args);
14 }
15
16}
9.15 启动项目输出 planUML 流程过程
在上面示例中,我们创建订单状态机配置时,在第 (4)
步骤中,设置状态机在被创建时,输出订单状态机转换的 plantUML
流程。所以,当 SpringBoot 项目启动后,就会在控制台输出如下的 plantuml
流程描述:
1@startuml
2CREATED_已创建 --> PENDING_SHIPMENT_待发货 : PAY_支付
3PENDING_SHIPMENT_待发货 --> SHIPPED_已发货 : SHIP_发货
4SHIPPED_已发货 --> RECEIVED_已收货 : RECEIVE_收货
5CREATED_已创建 --> CLOSED_已关闭 : CANCEL_取消
6PENDING_SHIPMENT_待发货 --> CLOSED_已关闭 : CANCEL_取消
7SHIPPED_已发货 --> CLOSED_已关闭 : CANCEL_取消
8@enduml
我们可以打开 plantuml 这个网址,然后将上面的内容输入到 plantuml 的输入框中,然后我们就可以看到如下所示的订单状态流转图:
在图中展示的订单状态转换,就是我们在应用中配置的订单状态转换规则,可以根据图中的流转方向来确认,配置的规则是否正确。
9.16 创建订单状态机测试类
接下来,我们创建一个测试类,用于验证当指定事件发生后,对应的订单状态流转是否正确。测试类代码如下:
1import club.mydlq.example.Application;
2import org.junit.jupiter.api.MethodOrderer;
3import org.junit.jupiter.api.Order;
4import org.junit.jupiter.api.Test;
5import org.junit.jupiter.api.TestMethodOrder;
6import org.springframework.beans.factory.annotation.Autowired;
7import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
8import org.springframework.boot.test.context.SpringBootTest;
9import org.springframework.test.web.servlet.MockMvc;
10import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
11import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
12
13/**
14 * 订单状态转换测试
15 *
16 * @author mydlq
17 */
18@AutoConfigureMockMvc
19@SpringBootTest(classes = Application.class)
20@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
21public class OrderControllerTest {
22
23 @Autowired
24 private MockMvc mockMvc;
25
26 /**
27 * 调用下单接口,获取一个订单ID
28 */
29 public String createOrder() throws Exception {
30 // 模拟调用下单接口,获取一个订单ID
31 return mockMvc.perform(post("/order/create"))
32 .andExpect(status().isOk())
33 .andReturn()
34 .getResponse()
35 .getContentAsString();
36 }
37
38 /**
39 * (1) 测试状态转换: 已创建 -> (支付) -> 待发货
40 */
41 @Test
42 @Order(1)
43 public void testCreatedToPendingShipment() throws Exception {
44 System.out.println("---(1) 测试状态转换: 已创建 -> (支付) -> 待发货---");
45 String orderId = createOrder(); //下单
46 mockMvc.perform(post("/order/pay").param("orderId", orderId)); //支付
47 }
48
49 /**
50 * (2) 测试状态转换: 待发货 -> (发货) -> 已发货
51 */
52 @Test
53 @Order(2)
54 public void testPendingShipmentToShipped() throws Exception {
55 System.out.println("\n---(2) 测试状态转换: 待发货 -> (发货) -> 已发货---" );
56 String orderId = createOrder(); //下单
57 mockMvc.perform(post("/order/pay").param("orderId", orderId)); //支付
58 mockMvc.perform(post("/order/ship").param("orderId", orderId)); //发货
59 }
60
61 /**
62 * (3) 测试状态转换: 已发货 -> (收货) -> 已收货
63 */
64 @Test
65 @Order(3)
66 public void testShippedToReceived() throws Exception {
67 System.out.println("\n---(3) 测试状态转换: 已发货 -> (收货) -> 已收货---");
68 String orderId = createOrder(); //下单
69 mockMvc.perform(post("/order/pay").param("orderId", orderId)); //支付
70 mockMvc.perform(post("/order/ship").param("orderId", orderId)); //发货
71 mockMvc.perform(post("/order/receive").param("orderId", orderId)); //收货
72 }
73
74 /**
75 * (4) 测试状态转换: 已创建 -> (取消) -> 已取消
76 */
77 @Test
78 @Order(4)
79 public void testCreatedToCanceled() throws Exception {
80 System.out.println("\n---(4) 测试状态转换: 已创建 -> (取消) -> 已关闭---");
81 String orderId = createOrder(); //下单
82 mockMvc.perform(post("/order/cancel").param("orderId", orderId)); //取消
83 }
84
85 /**
86 * (5) 测试状态转换: 待发货 -> (取消) -> 已取消
87 */
88 @Test
89 @Order(5)
90 public void testPendingShipmentToCanceled() throws Exception {
91 System.out.println("\n---(5) 测试状态转换: 待发货 -> (取消) -> 已关闭---");
92 String orderId = createOrder(); //下单
93 mockMvc.perform(post("/order/pay").param("orderId", orderId)); //支付
94 mockMvc.perform(post("/order/cancel").param("orderId", orderId)); //取消
95 }
96
97 /**
98 * (6) 测试状态转换: 已发货 -> (取消) -> 已取消
99 */
100 @Test
101 @Order(6)
102 public void testShippedToCanceled() throws Exception {
103 System.out.println("\n---(6) 测试状态转换: 已发货 -> (取消) -> 已关闭---");
104 String orderId = createOrder(); //下单
105 mockMvc.perform(post("/order/pay").param("orderId", orderId)); //支付
106 mockMvc.perform(post("/order/ship").param("orderId", orderId)); //发货
107 mockMvc.perform(post("/order/cancel").param("orderId", orderId)); //取消
108 }
109
110}
9.17 启动测试类进行验证
之后,启动测试类 OrderControllerTest
,来验证配置的转换规则是否正确,启动后控制台输入内容如下:
1---(1) 测试状态转换: 已创建 -> (支付) -> 待发货---
2订单ID:20240816182480001: CREATED_已创建 -> (PAY_支付) -> PENDING_SHIPMENT_待发货
3
4---(2) 测试状态转换: 待发货 -> (发货) -> 已发货---
5订单ID:20240816184070002: CREATED_已创建 -> (PAY_支付) -> PENDING_SHIPMENT_待发货
6订单ID:20240816184070002: PENDING_SHIPMENT_待发货 -> (SHIP_发货) -> SHIPPED_已发货
7
8---(3) 测试状态转换: 已发货 -> (收货) -> 已收货---
9订单ID:20240816184480003: CREATED_已创建 -> (PAY_支付) -> PENDING_SHIPMENT_待发货
10订单ID:20240816184480003: PENDING_SHIPMENT_待发货 -> (SHIP_发货) -> SHIPPED_已发货
11订单ID:20240816184480003: SHIPPED_已发货 -> (RECEIVE_收货) -> RECEIVED_已收货
12
13---(4) 测试状态转换: 已创建 -> (取消) -> 已关闭---
14订单ID:20240816185000004: CREATED_已创建 -> (CANCEL_取消) -> CLOSED_已关闭
15
16---(5) 测试状态转换: 待发货 -> (取消) -> 已关闭---
17订单ID:20240816185290005: CREATED_已创建 -> (PAY_支付) -> PENDING_SHIPMENT_待发货
18订单ID:20240816185290005: PENDING_SHIPMENT_待发货 -> (CANCEL_取消) -> CLOSED_已关闭
19
20---(6) 测试状态转换: 已发货 -> (取消) -> 已关闭---
21订单ID:20240816185630006: CREATED_已创建 -> (PAY_支付) -> PENDING_SHIPMENT_待发货
22订单ID:20240816185630006: PENDING_SHIPMENT_待发货 -> (SHIP_发货) -> SHIPPED_已发货
23订单ID:20240816185630006: SHIPPED_已发货 -> (CANCEL_取消) -> CLOSED_已关闭
可以看到,测试结果符合预期。
--- END ---
!版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。