一、这篇文章解决什么问题
前两篇文章已经把这些事情讲清楚了:
- 状态机是什么
- 为什么订单场景适合状态机
- 状态机为什么要结合快照、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 了。
这时候你创建状态机时,不能总是从初始状态开始,而是要把机器恢复到订单当前状态。
也就是说,真实项目一般会做两件事:
- 先从数据库查出订单当前状态
- 再把状态机重置到这个状态
这是很多初学者一开始最容易忽略的点。
十二、项目里更推荐怎么用
如果你真要在项目里落地,通常不会直接像 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 扫描后再走同一套状态流转入口。
这段话的重点是:
- 你不是只会背框架名
- 你知道它怎么和业务入口结合
- 你知道它不是孤立存在的
十六、最后总结
如果你只记住这篇文章最核心的内容,可以记下面六句话:
Spring Statemachine本质上是在帮你管理"状态 + 事件 + 流转规则"。- 最小落地只需要三步:定义状态、定义事件、配置转移。
sendEvent(...)才是真正触发状态流转的动作。- 非法流转不会通过,这正是状态机的价值。
- 真实项目里一定要考虑状态恢复、落库、快照和补偿。
- 框架只是工具,核心仍然是你对业务规则的建模能力。