Spring 状态机
文章目录
!版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。
一、什么状态机
状态机一般指有限状态机 (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
、Cola StateMachine
等。其中,Spring StateMachine
和 Squirrel
框架提供了非常丰富的功能,如状态嵌套、子状态、状态持久化等。然而,这些框架的全面性也带来了额外的复杂性,可能会让初次使用者感到难以掌握。
对于大多数实际项目而言,并不需要这些高级特性,如状态嵌套、并行状态、子状态机等。在简单的项目中,这些特性通常是不必要的。此外,这些框架通常是有状态的,这意味着在多实例部署或多线程环境下使用时,需要特别注意线程安全问题。为了确保线程安全,开发者往往需要引入锁机制,而这可能会对性能产生一定的负面影响。
相比之下,Cola StateMachine
框架是阿里开源的一个无状态的状态机框架,其设计非常简洁,易于上手,因此对于大多数实际项目来说,它已经足够使用了。
7.2 Cola StateMachine 框架简介
7.3 Cola StateMachine 框架入门
八、Cola StateMachine 使用示例
8.1 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 <!--test-->
35 <dependency>
36 <groupId>org.springframework.boot</groupId>
37 <artifactId>spring-boot-starter-test</artifactId>
38 <scope>test</scope>
39 </dependency>
40 <!--lombok-->
41 <dependency>
42 <groupId>org.projectlombok</groupId>
43 <artifactId>lombok</artifactId>
44 <optional>true</optional>
45 </dependency>
46 </dependencies>
47
48 <build>
49 <plugins>
50 <plugin>
51 <groupId>org.springframework.boot</groupId>
52 <artifactId>spring-boot-maven-plugin</artifactId>
53 </plugin>
54 </plugins>
55 </build>
56
57</project>
8.2 定义状态和事件枚举
订单事件枚举
1import lombok.AllArgsConstructor;
2import lombok.Getter;
3
4/**
5 * 订单事件枚举
6 *
7 * @author mydlq
8 */
9@Getter
10@AllArgsConstructor
11public enum OrderEvent {
12 /**
13 * 订单事件类型
14 */
15 PAY(1, "支付" ),
16 SHIP(2, "发货" ),
17 RECEIVE(3, "收货" ),
18 CANCEL(4, "取消" ),
19 ;
20
21 private final Integer code;
22 private final String desc;
23
24 @Override
25 public String toString() {
26 return this.name() + "_" + desc;
27 }
28}
订单状态枚举
1import lombok.AllArgsConstructor;
2import lombok.Getter;
3
4/**
5 * 订单状态枚举
6 *
7 * @author mydlq
8 */
9@Getter
10@AllArgsConstructor
11public enum OrderState {
12 /**
13 * 订单状态类型
14 */
15 // 初态
16 CREATED(1, "已创建"),
17 PENDING_SHIPMENT(2, "待发货"),
18 SHIPPED(3, "已发货"),
19 RECEIVED(4, "已收货"),
20 // 终态
21 CANCELED(5, "已取消"),
22 CLOSED(6, "已关闭"),
23 ;
24
25 private final Integer code;
26 private final String desc;
27
28 @Override
29 public String toString() {
30 return this.name() + "_" + desc;
31 }
32}
8.3 订单信息实体类
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.4 配置状态机以及状态转换规则
1import club.mydlq.example.enums.OrderEvent;
2import club.mydlq.example.enums.OrderState;
3import club.mydlq.example.model.Order;
4import com.alibaba.cola.statemachine.Action;
5import com.alibaba.cola.statemachine.Condition;
6import com.alibaba.cola.statemachine.StateMachine;
7import com.alibaba.cola.statemachine.builder.StateMachineBuilder;
8import com.alibaba.cola.statemachine.builder.StateMachineBuilderFactory;
9import org.springframework.context.annotation.Bean;
10import org.springframework.context.annotation.Configuration;
11
12/**
13 * 定义状态机和转换规则
14 *
15 * @author mydlq
16 */
17@Configuration
18public class StateMachineConfig {
19
20 @Bean("orderStateMachine")
21 public StateMachine<OrderState, OrderEvent, Order> orderStateMachine() {
22 // (1) 生成一个状态机builder
23 StateMachineBuilder<OrderState, OrderEvent, Order> builder = StateMachineBuilderFactory.create();
24
25 // (2) 通过使用builder配置外部状态转换
26 // CREATED_已创建 --> PENDING_SHIPMENT_待发货 : PAY_支付
27 builder.externalTransition()
28 .from(OrderState.CREATED).to(OrderState.PENDING_SHIPMENT).on(OrderEvent.PAY)
29 .when(checkCondition())
30 .perform(doAction());
31 // CREATED_已创建 --> CANCELED_已取消 : CANCEL_取消
32 builder.externalTransition()
33 .from(OrderState.CREATED).to(OrderState.CANCELED).on(OrderEvent.CANCEL)
34 .when(checkCondition())
35 .perform(doAction());
36 // PENDING_SHIPMENT_待发货 --> CANCELED_已取消 : CANCEL_取消
37 builder.externalTransition()
38 .from(OrderState.PENDING_SHIPMENT).to(OrderState.CANCELED).on(OrderEvent.CANCEL)
39 .when(checkCondition())
40 .perform(doAction());
41 // PENDING_SHIPMENT_待发货 --> SHIPPED_已发货 : SHIP_发货
42 builder.externalTransition()
43 .from(OrderState.PENDING_SHIPMENT).to(OrderState.SHIPPED).on(OrderEvent.SHIP)
44 .when(checkCondition())
45 .perform(doAction());
46 // SHIPPED_已发货 --> CANCELED_已取消 : CANCEL_取消
47 builder.externalTransition()
48 .from(OrderState.SHIPPED).to(OrderState.CANCELED).on(OrderEvent.CANCEL)
49 .when(checkCondition())
50 .perform(doAction());
51 // SHIPPED_已发货 --> RECEIVED_已收货 : RECEIVE_收货
52 builder.externalTransition()
53 .from(OrderState.SHIPPED).to(OrderState.RECEIVED).on(OrderEvent.RECEIVE)
54 .when(checkCondition())
55 .perform(doAction());
56
57 // (3) 设置状态机的ID,并构建状态机
58 String machineId = "orderStateMachine";
59 StateMachine<OrderState, OrderEvent, Order> orderStateMachine = builder.build(machineId);
60
61 // (4) 输出状态机转换流程plantUML
62 System.out.println(orderStateMachine.generatePlantUML());
63
64 return orderStateMachine;
65 }
66
67 /**
68 * 状态转换的条件
69 */
70 private Condition<Order> checkCondition() {
71 return order -> {
72 // 进行条件检查,符合条件则返回true
73 return true;
74 };
75 }
76
77 /**
78 * 状态转换执行的动作
79 */
80 private Action<OrderState, OrderEvent, Order> doAction() {
81 return (from, to, event, order) -> {
82 System.out.println("订单ID=" + order.getOrderId() + ", from:" + from + " to:" + to + " on:" + event);
83 };
84 }
85
86}
8.5 创建订单 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}
8.6 创建订单 Mapper 类
1import club.mydlq.example.model.Order;
2import org.springframework.stereotype.Repository;
3import java.util.HashMap;
4import java.util.Map;
5
6/**
7 * 订单 Mapper
8 *
9 * @author mydlq
10 */
11@Repository
12public class OrderMapper {
13
14 /**
15 * 模拟数据库存储
16 */
17 private static final Map<Long, Order> ORDER_MAP = new HashMap<>();
18
19 /**
20 * 保存订单信息
21 * @param order 订单信息
22 */
23 public void save(Order order) {
24 ORDER_MAP.put(order.getOrderId(), order);
25 }
26
27 /**
28 * 查询订单信息
29 * @param orderId 订单ID
30 * @return 订单信息
31 */
32 public Order findById(Long orderId) {
33 return ORDER_MAP.get(orderId);
34 }
35
36 /**
37 * 更新订单信息
38 * @param order 订单信息
39 */
40 public void update(Order order) {
41 ORDER_MAP.put(order.getOrderId(), order);
42 }
43
44}
8.7 创建订单 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 com.alibaba.cola.statemachine.StateMachine;
7import jakarta.annotation.Resource;
8import org.springframework.stereotype.Service;
9
10/**
11 * 订单 Service
12 *
13 * @author mydlq
14 */
15@Service
16public class OrderServiceImpl implements OrderService {
17 /**
18 * 订单持久化 Mapper
19 */
20 @Resource
21 private OrderMapper orderMapper;
22 /**
23 * 订单状态机
24 */
25 @Resource(name = "orderStateMachine")
26 StateMachine<OrderState, OrderEvent, Order> orderOperaMachine;
27
28 @Override
29 public Long create() {
30 // 生成订单ID (为了方便,这里生成一个10000-99999的随机数作为订单ID))
31 Long orderId = (long) (Math.random() * 90000) + 10000;
32 // 创建订单并设置订单初始状态
33 Order order = new Order(orderId, OrderState.CREATED);
34 // 执行订单创建的其它操作
35 // ...
36 // 保存订单信息
37 orderMapper.save(order);
38 return orderId;
39 }
40
41 @Override
42 public void pay(Long orderId) {
43 // 查询订单信息
44 Order order = orderMapper.findById(orderId);
45 // 执行支付操作
46 // ...
47 // 触发状态机状态变更
48 OrderState targetState = orderOperaMachine.fireEvent(order.getOrderState(), OrderEvent.PAY, order);
49 // 更新订单状态
50 order.setOrderState(targetState);
51 orderMapper.update(order);
52 }
53
54 @Override
55 public void ship(Long orderId) {
56 // 查询订单信息
57 Order order = orderMapper.findById(orderId);
58 // 执行发货操作
59 // ...
60 // 触发状态机状态变更
61 OrderState targetState = orderOperaMachine.fireEvent(order.getOrderState(), OrderEvent.SHIP, order);
62 // 更新订单状态
63 order.setOrderState(targetState);
64 orderMapper.update(order);
65 }
66
67 @Override
68 public void receive(Long orderId) {
69 // 查询订单信息
70 Order order = orderMapper.findById(orderId);
71 // 执行确认收货操作
72 // ...
73 // 触发状态机状态变更
74 OrderState targetState = orderOperaMachine.fireEvent(order.getOrderState(), OrderEvent.RECEIVE, order);
75 // 更新订单状态
76 order.setOrderState(targetState);
77 orderMapper.update(order);
78 }
79
80 @Override
81 public void cancel(Long orderId) {
82 // 查询订单信息
83 Order order = orderMapper.findById(orderId);
84 // 执行取消订单操作
85 // ...
86 // 触发状态机状态变更
87 OrderState targetState = orderOperaMachine.fireEvent(order.getOrderState(), OrderEvent.CANCEL, order);
88 // 更新订单状态
89 order.setOrderState(targetState);
90 orderMapper.update(order);
91 }
92
93}
8.8 创建订单 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}
8.9 创建 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}
8.10 启动项目输出 planUML 流程过程
在上面示例中,我们创建订单状态机配置时,在第 (4)
步骤中,设置状态机在被创建时,输出订单状态机转换的 plantUML
流程。所以,当 SpringBoot 项目启动后,就会在控制台输出如下的 plantuml
流程描述:
1@startuml
2CREATED_已创建 --> PENDING_SHIPMENT_待发货 : PAY_支付
3PENDING_SHIPMENT_待发货 --> SHIPPED_已发货 : SHIP_发货
4SHIPPED_已发货 --> RECEIVED_已收货 : RECEIVE_收货
5CREATED_已创建 --> CANCELED_已取消 : CANCEL_取消
6PENDING_SHIPMENT_待发货 --> CANCELED_已取消 : CANCEL_取消
7SHIPPED_已发货 --> CANCELED_已取消 : CANCEL_取消
8@enduml
我们可以打开 plantuml 这个网址,然后将上面的内容输入到 plantuml 的输入框中:
然后,我们就可以看到如下所示的订单状态流转图:
在图中展示的订单状态转换,就是我们在应用中配置的订单状态转换规则,可以根据图中的流转方向来确认,配置的规则是否正确。
8.11 创建订单状态机测试类
接下来,我们创建一个测试类,用于验证当指定事件发生后,对应的订单状态流转是否正确。测试类代码如下:
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}
8.12 启动测试类进行验证
之后,启动测试类 OrderControllerTest
,来验证配置的转换规则是否正确,启动后控制台输入内容如下:
1---(1) 测试状态转换: 已创建 -> (支付) -> 待发货---
2订单ID=10833, from:CREATED_已创建 to:PENDING_SHIPMENT_待发货 on:PAY_支付
3
4---(2) 测试状态转换: 待发货 -> (发货) -> 已发货---
5订单ID=25371, from:CREATED_已创建 to:PENDING_SHIPMENT_待发货 on:PAY_支付
6订单ID=25371, from:PENDING_SHIPMENT_待发货 to:SHIPPED_已发货 on:SHIP_发货
7
8---(3) 测试状态转换: 已发货 -> (收货) -> 已收货---
9订单ID=71923, from:CREATED_已创建 to:PENDING_SHIPMENT_待发货 on:PAY_支付
10订单ID=71923, from:PENDING_SHIPMENT_待发货 to:SHIPPED_已发货 on:SHIP_发货
11订单ID=71923, from:SHIPPED_已发货 to:RECEIVED_已收货 on:RECEIVE_收货
12
13---(4) 测试状态转换: 已创建 -> (取消) -> 已取消---
14订单ID=72889, from:CREATED_已创建 to:CANCELED_已取消 on:CANCEL_取消
15
16---(5) 测试状态转换: 待发货 -> (取消) -> 已取消---
17订单ID=48526, from:CREATED_已创建 to:PENDING_SHIPMENT_待发货 on:PAY_支付
18订单ID=48526, from:PENDING_SHIPMENT_待发货 to:CANCELED_已取消 on:CANCEL_取消
19
20---(6) 测试状态转换: 已发货 -> (取消) -> 已取消---
21订单ID=47479, from:CREATED_已创建 to:PENDING_SHIPMENT_待发货 on:PAY_支付
22订单ID=47479, from:PENDING_SHIPMENT_待发货 to:SHIPPED_已发货 on:SHIP_发货
23订单ID=47479, from:SHIPPED_已发货 to:CANCELED_已取消 on:CANCEL_取消
--- END ---
!版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。