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. 框架只是工具,核心仍然是你对业务规则的建模能力。
相关推荐
早日退休!!!1 小时前
操作系统锁
java·开发语言
研究点啥好呢1 小时前
快手多模态算法工程师面试题精选:10道高频考题+答案解析
java·开发语言·人工智能·ai·面试·笔试
遗憾随她而去.1 小时前
Java学习(一)
java·开发语言·学习
陌路物是人非1 小时前
记一个controller入参为null的奇怪问题
java·开发语言
小瓦码J码1 小时前
Spring boot 如何自定义加密解密数据库连接配置
java
XiYang-DING2 小时前
【Java EE】JUC的常见类(Callable、ReentrantLock、Semaphore和CountDownLatch )
java·java-ee
RuoyiOffice2 小时前
2026 年开源 BPM/工作流引擎大盘点:Flowable vs Camunda vs Activiti vs Turbo——谁才是企业级首选?
java·spring boot·后端·开源·流程图·ruoyi·anti-design-vue
SamDeepThinking2 小时前
别把业务逻辑塞进存储过程,适当用表驱动法
java·后端·架构
HZY1618yzh2 小时前
洛谷题解:P16304 [蓝桥杯 2026 省 Java C 组] 抽奖活动
java·c++·算法·蓝桥杯