Spring 状态机

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/elseswitch 语句来处理这些逻辑。然而,如果业务逻辑变得复杂,这将会导致大量的 if/elseswitch 语句,从而使代码结构变得臃肿且难以维护。而且,每增加一种新的状态或条件,都需要添加更多的判断逻辑,这不仅增加了出错的风险,也让代码变得难以阅读和理解,无法清晰地表达系统的状态和行为。

相比之下,状态机提供了一种更结构化、易于理解和维护的方式来管理系统的状态和行为。每个状态都代表了一个特定的情况,而从一个状态到另一个状态的转换则是由明确的事件触发。这种方法将问题分解成更小、更易管理的部分。例如,在订单状态流转的例子中,我们可以定义“未支付”、“已支付待发货”、“已发货”、“已签收”等状态。当接收到支付成功的事件时,订单就会从“未支付”转换为“已支付待发货”的状态。这种明确的状态转换规则,在复杂的业务逻辑中能极大地提高代码的可读性和可维护性。

总而言之,使用状态机进行设计可以显著提升代码的可读性和可维护性。它可以将复杂的条件判断拆分为明确的状态和转换,不仅使代码更容易理解,也使团队中的其他开发人员更容易接手和维护。因此,在需要管理复杂状态逻辑的场景中,采用状态机是一种更为优雅和高效的设计选择。

三、状态机的几个概念

在状态机中,有以下几个基本概念需要了解:

  • 状态(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 的数据复制到输入框中,就可以看到如下所示的订单状态流转图:

planUML地址

通过图中的状态转换流程,可以确认代码中的状态转换规则是没有问题的。

七、常见开源的状态机框架

7.1 常见的开源框架

虽然使用枚举的方式实现状态机很简单直观,但随着业务场景的复杂度增加,这种简单的枚举状态机可能无法满足非线性的状态流转需求。在这种情况下,可以选择扩展现有的状态机实现,或者采用功能更为全面的开源状态机框架。

有许多成熟的开源状态机框架可供选择,例如 Spring StateMachineSquirrelCola StateMachine 等。其中,Spring StateMachineSquirrel 框架提供了非常丰富的功能,如状态嵌套、子状态、状态持久化等。然而,这些框架的全面性也带来了额外的复杂性,可能会让初次使用者感到难以掌握。

对于大多数实际项目而言,并不需要这些高级特性,如状态嵌套、并行状态、子状态机等。在简单的项目中,这些特性通常是不必要的。此外,这些框架通常是有状态的,这意味着在多实例部署或多线程环境下使用时,需要特别注意线程安全问题。为了确保线程安全,开发者往往需要引入锁机制,而这可能会对性能产生一定的负面影响。

相比之下,Cola StateMachine 框架是阿里开源的一个无状态的状态机框架,其设计非常简洁,易于上手,因此对于大多数实际项目来说,它已经足够使用了。

7.2 Cola StateMachine 框架简介

7.3 Cola StateMachine 框架入门

八、Cola StateMachine 使用示例

8.1 Maven 引入相关依赖

在 Maven 配置文件 pom.xml 中添加 spring-bootcola-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 的输入框中:

planUML地址

然后,我们就可以看到如下所示的订单状态流转图:

订单状态转换UML图

在图中展示的订单状态转换,就是我们在应用中配置的订单状态转换规则,可以根据图中的流转方向来确认,配置的规则是否正确。

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 ---


  !版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。