SpringBoot 遇上状态机:简化复杂业务逻辑的利器

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/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 StateMachineSquirrel StateMachineCola StateMachine 等。其中,Spring StateMachineSquirrel StateMachine 框架提供了非常丰富的功能,如状态嵌套、状态子状态机等。然而,这些框架的全面性也带来了额外的复杂性,可能会让初次使用者感到难以掌握。

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

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

7.2 三款框架简单对比

这里大致对 Spring StateMachineSquirrel StateMachineCola 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-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    <!--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 目录中,创建对应的 MyBatisxml 文件,内如如下:

 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 的输入框中,然后我们就可以看到如下所示的订单状态流转图:

planUML地址

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

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


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