Spring Statemachine 实战入门:从零实现一个订单状态流转 Demo

一、这篇文章解决什么问题

前两篇文章已经把这些事情讲清楚了:

  • 状态机是什么
  • 为什么订单场景适合状态机
  • 状态机为什么要结合快照、XXL-JOB 和补偿机制

但如果你是第一次接触 Spring Statemachine,还是会卡在一个很实际的问题上:

概念我懂了,但代码到底怎么写?

这篇文章就只做一件事:

从零搭一个最小 Demo,让你看明白 Spring Statemachine 在代码里到底长什么样。

目标很简单:

  • 定义订单状态
  • 定义触发事件
  • 配置允许的流转规则
  • 写一个最小测试流程

这篇文章不会一上来就讲特别复杂的高级特性,而是先让你把最基础的一套跑通。

二、先明确一个最小订单流转场景

为了降低理解成本,我们先只做最小版本。

订单状态定义成 5 个:

  • 待支付
  • 待接单
  • 待服务
  • 已完成
  • 已取消

事件定义成 4 个:

  • 支付成功
  • 接单成功
  • 完成服务
  • 取消订单

它们之间的关系可以先理解成:

  • 待支付 --支付成功--> 待接单
  • 待支付 --取消订单--> 已取消
  • 待接单 --接单成功--> 待服务
  • 待服务 --完成服务--> 已完成

这个例子虽然小,但已经足够让你理解 Spring Statemachine 的核心写法。

三、先看状态机的核心组成

Spring Statemachine 写状态机时,你可以先把代码理解成三部分:

3.1 状态定义

也就是有哪些状态。

3.2 事件定义

也就是哪些动作会触发状态变化。

3.3 流转配置

也就是从什么状态,在什么事件下,可以变成什么状态。

如果这三部分你能看懂,其实就已经入门了。

四、准备依赖

如果你是 Spring Boot 项目,先引入依赖。

xml 复制代码
<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-starter</artifactId>
    <version>3.2.0</version>
</dependency>

如果你的项目里已经有统一的依赖管理,就按你们项目版本来,不一定非得跟这里一模一样。

重点不是版本号,而是你知道要引这个 starter。

五、先定义状态和事件枚举

这一步最简单。

5.1 订单状态枚举

java 复制代码
public enum OrderStatus {
    TO_PAY,
    TO_DISPATCH,
    TO_SERVICE,
    FINISHED,
    CANCELED
}

5.2 订单事件枚举

java 复制代码
public enum OrderEvent {
    PAY_SUCCESS,
    DISPATCH_SUCCESS,
    FINISH_SERVICE,
    CANCEL
}

到这里其实和你自己手写状态机没太大区别。

真正开始进入 Spring Statemachine 的地方,是下面的配置类。

六、最核心的配置类怎么写

你可以先把它理解成一句话:

把状态和流转关系交给框架注册进去。

java 复制代码
@Configuration
@EnableStateMachineFactory
public class OrderStateMachineConfig
        extends StateMachineConfigurerAdapter<OrderStatus, OrderEvent> {

    @Override
    public void configure(StateMachineStateConfigurer<OrderStatus, OrderEvent> states)
            throws Exception {
        states.withStates()
                .initial(OrderStatus.TO_PAY)
                .states(EnumSet.allOf(OrderStatus.class));
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<OrderStatus, OrderEvent> transitions)
            throws Exception {
        transitions
                .withExternal()
                    .source(OrderStatus.TO_PAY)
                    .target(OrderStatus.TO_DISPATCH)
                    .event(OrderEvent.PAY_SUCCESS)
                .and()
                .withExternal()
                    .source(OrderStatus.TO_PAY)
                    .target(OrderStatus.CANCELED)
                    .event(OrderEvent.CANCEL)
                .and()
                .withExternal()
                    .source(OrderStatus.TO_DISPATCH)
                    .target(OrderStatus.TO_SERVICE)
                    .event(OrderEvent.DISPATCH_SUCCESS)
                .and()
                .withExternal()
                    .source(OrderStatus.TO_SERVICE)
                    .target(OrderStatus.FINISHED)
                    .event(OrderEvent.FINISH_SERVICE);
    }
}

七、这段配置到底在表达什么

很多人第一次看到这个类,会觉得写法有点绕。

其实拆开看就很简单。

7.1 @EnableStateMachineFactory 是什么意思

它表示:

不要只创建一个固定状态机,而是创建一个"状态机工厂"。

为什么项目里更常用工厂?

因为真实系统里不是只有一张订单。

你可能有:

  • 订单 A 的状态机
  • 订单 B 的状态机
  • 订单 C 的状态机

也就是说,通常需要按业务对象去创建状态机实例,而不是全局只用一个。

7.2 initial(...) 是什么意思

java 复制代码
.initial(OrderStatus.TO_PAY)

表示状态机初始状态是"待支付"。

也就是说,订单刚创建出来,默认先落在这个状态。

7.3 withExternal() 是什么意思

你可以先把它简单理解成:

定义一次从一个状态到另一个状态的外部流转。

例如:

java 复制代码
.source(OrderStatus.TO_PAY)
.target(OrderStatus.TO_DISPATCH)
.event(OrderEvent.PAY_SUCCESS)

意思就是:

当前状态如果是 TO_PAY,收到事件 PAY_SUCCESS,那就流转到 TO_DISPATCH

这就是状态机配置最核心的一行。

八、怎么真正触发状态流转

状态机配置好了,不代表它会自己动。

你还是要主动发送事件。

先写一个简单的 service。

java 复制代码
@Service
public class OrderStateMachineService {

    private final StateMachineFactory<OrderStatus, OrderEvent> stateMachineFactory;

    public OrderStateMachineService(
            StateMachineFactory<OrderStatus, OrderEvent> stateMachineFactory) {
        this.stateMachineFactory = stateMachineFactory;
    }

    public StateMachine<OrderStatus, OrderEvent> create() {
        StateMachine<OrderStatus, OrderEvent> stateMachine = stateMachineFactory.getStateMachine();
        stateMachine.start();
        return stateMachine;
    }
}

然后测试一下:

java 复制代码
@SpringBootTest
class OrderStateMachineTest {

    @Autowired
    private OrderStateMachineService orderStateMachineService;

    @Test
    void testFlow() {
        StateMachine<OrderStatus, OrderEvent> machine = orderStateMachineService.create();

        System.out.println(machine.getState().getId());

        machine.sendEvent(OrderEvent.PAY_SUCCESS);
        System.out.println(machine.getState().getId());

        machine.sendEvent(OrderEvent.DISPATCH_SUCCESS);
        System.out.println(machine.getState().getId());

        machine.sendEvent(OrderEvent.FINISH_SERVICE);
        System.out.println(machine.getState().getId());
    }
}

预期结果就是:

text 复制代码
TO_PAY
TO_DISPATCH
TO_SERVICE
FINISHED

如果能看到这条链路跑通,说明你已经把最小状态机用起来了。

九、非法状态流转会怎么样

状态机最大的价值之一,就是帮你拦非法流转。

例如:

当前状态还是 TO_PAY,你却直接发:

java 复制代码
machine.sendEvent(OrderEvent.FINISH_SERVICE);

这时候它不会按你的想法变成 FINISHED

因为你根本没有配置:

  • TO_PAY --FINISH_SERVICE--> FINISHED

也就是说,只有你显式声明过的路,状态机才会走。

这就是为什么状态机比到处 if-else 更清晰。

十、怎么在状态变化时挂动作

只改状态通常不够。

真实项目里你经常还要在流转时顺便做别的事,例如:

  • 记录状态变更日志
  • 写订单快照
  • 发通知
  • 执行后续业务逻辑

这时候就可以给 transition 绑定 action。

java 复制代码
@Bean
public Action<OrderStatus, OrderEvent> paySuccessAction() {
    return context -> {
        System.out.println("支付成功后执行动作");
    };
}

然后挂到流转上:

java 复制代码
.withExternal()
    .source(OrderStatus.TO_PAY)
    .target(OrderStatus.TO_DISPATCH)
    .event(OrderEvent.PAY_SUCCESS)
    .action(paySuccessAction())

这样当支付成功事件触发时:

  • 状态会变
  • 动作也会执行

这就开始接近真实业务了。

十一、真实项目里不会只用"默认初始状态"

上面的 Demo 有一个简化点:

每次都是从 TO_PAY 开始。

但真实项目里,一条订单可能已经在数据库里是 TO_SERVICE 了。

这时候你创建状态机时,不能总是从初始状态开始,而是要把机器恢复到订单当前状态。

也就是说,真实项目一般会做两件事:

  1. 先从数据库查出订单当前状态
  2. 再把状态机重置到这个状态

这是很多初学者一开始最容易忽略的点。

十二、项目里更推荐怎么用

如果你真要在项目里落地,通常不会直接像 Demo 里那样到处 sendEvent

更推荐包一层统一入口。

例如:

java 复制代码
public void transfer(Order order, OrderEvent event) {
    StateMachine<OrderStatus, OrderEvent> machine = restore(order.getStatus());
    boolean accepted = machine.sendEvent(event);
    if (!accepted) {
        throw new IllegalStateException("非法状态流转");
    }
    OrderStatus newStatus = machine.getState().getId();
    order.setStatus(newStatus);
    // 保存订单、写快照、记流水
}

这样业务代码只知道:

  • 给某个订单发某个事件

而不需要关心状态机内部怎么运转。

十三、你在项目里至少还要补这三件事

如果只是 Demo,到现在就够了。

但如果你要真落地,至少还要继续补三块。

13.1 状态持久化

状态机本身在内存里跑,但订单状态最终还是要落数据库。

13.2 状态变更流水

不能只改当前状态,还要记录这次变化是怎么发生的。

13.3 异常和补偿

如果状态变了,但后续动作失败了,怎么补救。

所以你会发现:

Spring Statemachine 只是状态机落地的一块,不是全部。

十四、初学者最容易踩的坑

14.1 坑一:把框架当成全部

框架只负责状态流转这块,不负责你的订单表、快照表、补偿表怎么设计。

14.2 坑二:忽略当前状态恢复

如果每次都从初始状态启动,那真实订单流转一定不对。

14.3 坑三:状态变了但没落库

内存里变了,不代表数据库变了。

这是 Demo 和真实项目最大的差别之一。

14.4 坑四:只配流转,不配动作

真实订单系统里,状态变化通常不会只是改个字段。

十五、面试里怎么讲 Spring Statemachine

如果面试官问你:

"你们状态机具体怎么实现的?"

你可以这样回答:

我们会先定义订单状态和事件,再用 Spring Statemachine 配置状态流转规则。

每个订单状态变化时,不是直接在业务代码里改 status,而是通过统一入口恢复当前状态机实例、发送事件、校验流转是否合法。

如果流转成功,再统一落库、写订单快照和状态变更流水。

像超时取消这类时间驱动动作,则由 XXL-JOB 扫描后再走同一套状态流转入口。

这段话的重点是:

  • 你不是只会背框架名
  • 你知道它怎么和业务入口结合
  • 你知道它不是孤立存在的

十六、最后总结

如果你只记住这篇文章最核心的内容,可以记下面六句话:

  1. Spring Statemachine 本质上是在帮你管理"状态 + 事件 + 流转规则"。
  2. 最小落地只需要三步:定义状态、定义事件、配置转移。
  3. sendEvent(...) 才是真正触发状态流转的动作。
  4. 非法流转不会通过,这正是状态机的价值。
  5. 真实项目里一定要考虑状态恢复、落库、快照和补偿。
  6. 框架只是工具,核心仍然是你对业务规则的建模能力。
相关推荐
MacroZheng19 分钟前
完美替代 Navicat!这款内置 AI 的数据库工具,太香了!
java·后端·mysql
SamDeepThinking1 小时前
从REST到gRPC,一个API选型的思考框架
java·后端·程序员
Zane19941 小时前
接口都能写默认实现了,为什么还需要抽象类
java·后端
未秃头的程序猿1 小时前
读Spring AI源码的方法——不是硬啃,是带着问题去翻
java·后端·spring
用户3126874877201 小时前
别再 Thread.sleep 硬等了!并发工具类到底怎么选?
java
Java内核笔记1 小时前
万字长文剖析 Spring Boot 4 自动配置机制源码:从 @EnableAutoConfiguration 到条件装配
java·后端
evans在进步1 小时前
Spring MVC 核心机制详解:全局异常处理、请求跳转与数据绑定
java·spring·mvc
weixin_BYSJ19871 小时前
springboot小区物业管理小程序---附源码45555
java·javascript·spring boot·python·django·flask·php
coder_lorraine2 小时前
Halo 2.x 回收站文章无法删除?一篇文章教你彻底解决
java·运维
索隆zoro2 小时前
Army 进阶:DML 操作体系与 Spring AI 集成
java