SpringBoot 集成 Statemachine的实战指南
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>一、Statemachine 简介</li><ul class="second_class_ul"><li>1.1 核心概念</li><li>1.2 主要特性</li><li>1.3 注解驱动开发方式</li><li>1.4 核心注解详解</li><ul class="third_class_ul"><li>1)@WithStateMachine 注解</li><li>2)@OnTransition 注解</li><li>3)@OnTransitionStart 注解</li><li>4)@OnTransitionEnd 注解</li></ul><li>1.5 包含的模块</li><ul class="third_class_ul"></ul></ul><li>二、知识回顾——状态模式</li><ul class="second_class_ul"><li>2.1 什么是状态模式?</li><ul class="third_class_ul"></ul><li>2.2 状态模式的优缺点</li><ul class="third_class_ul"></ul><li>2.3 状态模式的实现结构</li><ul class="third_class_ul"></ul></ul><li>三、SpringBoot 集成</li><ul class="second_class_ul"><li>3.1 Maven 依赖</li><ul class="third_class_ul"></ul><li>3.2 定义状态和事件枚举</li><ul class="third_class_ul"></ul><li>3.3 配置状态机</li><ul class="third_class_ul"></ul><li>3.4 使用注解实现状态监听器</li><ul class="third_class_ul"></ul><li>3.5 业务服务类</li><ul class="third_class_ul"></ul><li>3.6 控制器类</li><ul class="third_class_ul"></ul><li>3.7 测试结果</li><ul class="third_class_ul"></ul></ul><li>四、升级:状态机持久化</li><ul class="second_class_ul"><li>4.1 Maven 依赖</li><ul class="third_class_ul"></ul><li>4.2 自定义持久化类</li><ul class="third_class_ul"></ul><li>4.3 编写状态机工具类</li><ul class="third_class_ul"></ul><li>4.4 修改业务调用</li><ul class="third_class_ul"></ul><li>4.5 测试结果</li><ul class="third_class_ul"></ul></ul></ul></div><p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011014464324.jpg" /></p><ul><li><strong>官网地址</strong>:https://spring.io/projects/spring-statemachine#learn</li><li><strong>官方文档</strong>:https://docs.spring.io/spring-statemachine/docs/3.2.1/reference/#statemachine-getting-started</li></ul>
<p class="maodian"></p><h2>一、Statemachine 简介</h2>
<p><code>Spring Statemachine</code> 是一个由 Spring 团队提供的 <strong>轻量级状态机框架</strong>,它允许开发者以简便且强大的方式管理复杂的状态流转逻辑。该框架建立在 <strong>有限状态机(FSM)</strong> 的概念之上,提供了一种简洁且灵活的方式来定义、管理和执行状态机。</p>
<p class="maodian"></p><h3>1.1 核心概念</h3>
<ul><li><strong>状态(State)</strong>:系统可能处于的不同条件或模式,是状态机的核心组成单元。</li><li><strong>事件(Event)</strong>:触发状态转换的动作或消息,是引起状态机从当前状态迁移到新状态的原因。</li><li><strong>转换(Transition)</strong>:描述了在何种条件下,当接收到特定事件时,系统可以从一个状态转移到另一个状态。</li><li><strong>动作(Action)</strong>:在状态转换时执行的具体操作。</li></ul>
<p class="maodian"></p><h3>1.2 主要特性</h3>
<p>Spring Statemachine 提供了丰富的功能特性:</p>
<ul><li>易于使用的平面(一级)状态机,适用于简单的用例;</li><li>分层状态结构,以简化复杂的状态配置;</li><li>状态机区域提供更复杂的状态配置;</li><li>触发器、转换、守卫和动作的使用;</li><li>类型安全的配置适配器;</li><li>状态机事件监听器;</li><li>Spring IoC 集成将 Bean 与 状态机 相关联。</li></ul>
<p class="maodian"></p><h3>1.3 注解驱动开发方式</h3>
<p>为了简化开发,可以使用 Statemachine 的 <strong>注解驱动开发方式</strong>,特别是 <code>@OnTransition</code>、<code>@OnTransitionStart</code>、<code>@OnTransitionEnd</code> 和 <code>@WithStateMachine</code> 注解的使用,这些注解能够让我们以更加 <strong>声明式</strong> 和 <strong>简介</strong> 的方式处理状态转换逻辑。</p>
<p>注解驱动的开发方式具有以下优势:</p>
<ul><li><strong>代码简洁性</strong>:将状态转换逻辑直接注解在方法上,减少模板代码;</li><li><strong>关注点分离</strong>:业务逻辑与状态机配置清晰分离,提高可维护性;</li><li><strong>类型安全</strong>:编译时检查注解的正确性,减少运行时错误;</li><li><strong>可读性强</strong>:通过注解直观地表达状态转换的意图。</li></ul>
<p class="maodian"></p><h3>1.4 核心注解详解</h3>
<p class="maodian"></p><h4>1)@WithStateMachine 注解</h4>
<p><code>@WithStateMachine</code> 注解用于标识一个类是与状态机相关的监听器,它告诉 Spring 这个类中的方法需要接受状态机的事件通知。</p>
<ul><li><strong>使用场景</strong>:标记状态机监听器类,是类中的状态转换注解生效。</li></ul>
<p class="maodian"></p><h4>2)@OnTransition 注解</h4>
<p><code>@OnTrasition</code> 注解用于标记在状态转换发生时执行的方法,它不区分转换的开始和结束。</p>
<ul><li><strong>使用场景</strong>:当不关心转换的具体阶段,只需要在转换发生时执行某些逻辑时使用。</li></ul>
<p class="maodian"></p><h4>3)@OnTransitionStart 注解</h4>
<p><code>@OnTransitionStart</code> 注解用于标记在状态转换开始时执行的方法。</p>
<ul><li><strong>使用场景</strong>:需要在状态转换刚开始时执行与处理逻辑,如参数验证、资源准备等。</li></ul>
<p class="maodian"></p><h4>4)@OnTransitionEnd 注解</h4>
<p><code>@OnTransitionEnd</code> 注解用于标记在状态转换 结束时 执行的方法。</p>
<ul><li><strong>使用场景</strong>:需要在状态转换完成后执行清理逻辑、记录日志、发送通知等。</li></ul>
<p class="maodian"></p><h3>1.5 包含的模块</h3>
<p>Spring Statemachine 包含的模块如下:</p>
<table><thead><tr><th>模块</th><th>描述</th></tr></thead><tbody><tr><td><code>spring-statemachine-core</code></td><td>Spring Statemachine的核心系统。</td></tr><tr><td><code>spring-statemachine-recipes-common</code></td><td>不需要核心框架之外的依赖项的常见配方。</td></tr><tr><td><code>spring-statemachine-kryo</code></td><td><code>Kryo</code>Spring Statemachine的序列化程序。</td></tr><tr><td><code>spring-statemachine-data-common</code></td><td><code>Spring Data</code>的通用支持模块。</td></tr><tr><td><code>spring-statemachine-data-jpa</code></td><td>支持<code>Spring Data JPA</code>模块。</td></tr><tr><td><code>spring-statemachine-data-redis</code></td><td>支持<code>Spring Data Redis</code>模块。</td></tr><tr><td><code>spring-statemachine-data-mongodb</code></td><td>支持<code>Spring Data MongoDB</code>模块。</td></tr><tr><td><code>spring-statemachine-zookeeper</code></td><td>分布式状态机的Zooeman集成。</td></tr><tr><td><code>spring-statemachine-test</code></td><td>状态机测试支持模块。</td></tr><tr><td><code>spring-statemachine-cluster</code></td><td>Spring Cloud Cluster的支持模块。请注意,Spring Cloud Cluster已被Spring Integration取代。</td></tr><tr><td><code>spring-statemachine-uml</code></td><td>使用Eclipse Papyrus进行UI UML建模的支持模块。</td></tr><tr><td><code>spring-statemachine-autoconfigure</code></td><td>Spring Boot的支持模块。</td></tr><tr><td><code>spring-statemachine-bom</code></td><td>物料清单pom。</td></tr><tr><td><code>spring-statemachine-starter</code></td><td>弹簧启动启动器。</td></tr></tbody></table>
<p class="maodian"></p><h2>二、知识回顾——状态模式</h2>
<p class="maodian"></p><h3>2.1 什么是状态模式?</h3>
<p><code>状态模式(State Pattern)</code> 是一种 <strong>行为型</strong> 设计模式,对有状态的对象,把复杂的 “判断逻辑” 提取到不同的状态对象中,允许状态对象在其内部状态发生改变时,改变其行为。</p>
<p class="maodian"></p><h3>2.2 状态模式的优缺点</h3>
<p><strong>状态模式的优点:</strong></p>
<ul><li><strong>结构清晰</strong> :状态模式将与特定状态相关的行为局部化道一个状态中,并且将不同状态的行为分割开来,满足 “但一职责原则”。</li><li><strong>将状态转换显示化</strong>:减少对象间的相互依赖,将不同的状态引入独立的对象中会是的状态转换变得更加明确,且减少对相见的相互依赖。</li><li><strong>状态类职责明确</strong>:有利于程序的扩展。通过定义新的子类很容易地增加新的状态和转换。</li></ul>
<p><strong>状态模式的缺点:</strong></p>
<ol><li>状态模式的使用必然会增加系统的类与对象的个数。</li><li>状态模式的结构与实现都较为复杂,如果使用不当会导致程序结构和代码的混乱。</li><li>状态模式对开闭原则的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源码,否则无法切换到新增状态,而且修改某个状态类的行为也需要修改对应类的源码。</li></ol>
<p class="maodian"></p><h3>2.3 状态模式的实现结构</h3>
<p>状态模式把受环境改变的对象行为包装在不同的状态对象里,其意图是让一个对象在其内部状态改变的时候,其行为也随之改变。现在我们来分析其基本结构和实现方法。</p>
<p>状态模式主要包含三个角色:</p>
<ul><li><strong>Context(环境类)</strong>:定义客户端感兴趣的接口,维护一个 State 子类的实例,这个示例定义当前状态。</li><li><strong>State(抽象状态类)</strong>:定义一个接口,用以封装 Context 的特定状态相关的行为。</li><li><strong>ConcreteState(具体状态类)</strong>:每一个子类实现一个与 Context 的一个状态相关的行为。</li></ul>
<p class="maodian"></p><h2>三、SpringBoot 集成</h2>
<p>项目结构如下:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011014464336.jpg" /></p>
<p class="maodian"></p><h3>3.1 Maven 依赖</h3>
<p>对于 SpringBoot 2.x 项目,可以使用 2.x 版本的 StateMachine 依赖:</p>
<div class="jb51code"><pre class="brush:plain;"><dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-starter</artifactId>
<version>2.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-kryo</artifactId>
<version>2.2.3.RELEASE</version>
</dependency></pre></div>
<p class="maodian"></p><h3>3.2 定义状态和事件枚举</h3>
<p>首先,我们需要定义状态机和事件的所有可能值。以订单系统为例:</p>
<p><strong>OrderStatesEnum.java</strong>、<strong>OrderEventsEnum.java</strong></p>
<div class="jb51code"><pre class="brush:java;">public enum OrderStatesEnum {
UNPAID, // 待支付
WAITING_FOR_RECEIVE, // 待收货
DONE, // 完成
CANCELLED // 取消
}
public enum OrderEventsEnum {
PAY, // 支付
RECEIVE, // 收货
CANCEL // 取消
}</pre></div>
<p class="maodian"></p><h3>3.3 配置状态机</h3>
<p>接下来,我们需要配置状态机,定义状态转换规则:</p>
<p><strong>StateMachineConfig.java</strong></p>
<div class="jb51code"><pre class="brush:java;">import com.demo.enums.OrderEventsEnum;
import com.demo.enums.OrderStatesEnum;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import java.util.EnumSet;
@Configuration
@EnableStateMachine(name = "orderStateMachine")
public class StateMachineConfig extends StateMachineConfigurerAdapter<OrderStatesEnum, OrderEventsEnum> {
@Override
public void configure(StateMachineStateConfigurer<OrderStatesEnum, OrderEventsEnum> states) throws Exception {
states
.withStates()
.initial(OrderStatesEnum.UNPAID)
.states(EnumSet.allOf(OrderStatesEnum.class));
}
@Override
public void configure(StateMachineTransitionConfigurer<OrderStatesEnum, OrderEventsEnum> transitions) throws Exception {
transitions
.withExternal()
.source(OrderStatesEnum.UNPAID).target(OrderStatesEnum.WAITING_FOR_RECEIVE)
.event(OrderEventsEnum.PAY)
.and()
.withExternal()
.source(OrderStatesEnum.WAITING_FOR_RECEIVE).target(OrderStatesEnum.DONE)
.event(OrderEventsEnum.RECEIVE)
.and()
.withExternal()
.source(OrderStatesEnum.UNPAID).target(OrderStatesEnum.CANCELLED)
.event(OrderEventsEnum.CANCEL);
}
}</pre></div>
<p class="maodian"></p><h3>3.4 使用注解实现状态监听器</h3>
<p>这段代码展示如何使用注解来监听状态转换:</p>
<p><strong>OrderStateListener.java</strong></p>
<div class="jb51code"><pre class="brush:java;">import com.demo.enums.OrderEventsEnum;
import com.demo.enums.OrderStatesEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.annotation.OnTransition;
import org.springframework.statemachine.annotation.OnTransitionEnd;
import org.springframework.statemachine.annotation.OnTransitionStart;
import org.springframework.statemachine.annotation.WithStateMachine;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@WithStateMachine(name = "orderStateMachine")
public class OrderStateListener {
/**
* 支付转换开始时的处理
*/
@OnTransitionStart(source = "UNPAID", target = "WAITING_FOR_RECEIVE")
public void onPayStart(StateContext<OrderStatesEnum, OrderEventsEnum> context) {
log.info("【支付转换开始】开始处理支付逻辑");
// 获取转换相关的数据
Object paymentData = context.getMessageHeader("paymentData");
if (paymentData != null) {
log.info("支付数据:{}", paymentData);
}
// 执行支付前的验证逻辑
log.info("验证支付参数...");
log.info("检查库存...");
log.info("预扣库存...");
}
/**
* 支付转换结束时的处理
*/
@OnTransitionEnd(source = "UNPAID", target = "WAITING_FOR_RECEIVE")
public void onPayEnd(StateContext<OrderStatesEnum, OrderEventsEnum> context) {
log.info("【支付转换结束】支付处理完成");
// 执行支付后的清理逻辑
log.info("更新库存...");
log.info("生成支付凭证...");
log.info("发送支付成功通知...");
// 记录转换耗时
Long startTime = (Long) context.getMessageHeader("startTime");
if (startTime != null) {
long duration = System.currentTimeMillis() - startTime;
log.info("支付处理耗时:{}ms", duration);
}
}
/**
* 收货转换开始时的处理
*/
@OnTransitionStart(source = "WAITING_FOR_RECEIVE", target = "DONE")
public void onReceiveStart() {
log.info("【收货转换开始】开始确认收货");
log.info("验证收货权限...");
log.info("检查物流信息...");
}
/**
* 收货转换结束时的处理
*/
@OnTransitionEnd(source = "WAITING_FOR_RECEIVE", target = "DONE")
public void onReceiveEnd() {
log.info("【收货转换结束】收货确认完成");
log.info("更新订单完成时间...");
log.info("计算商家评分...");
log.info("发送订单完成通知...");
}
/**
* 取消订单转换开始时的处理
*/
@OnTransitionStart(source = "UNPAID", target = "CANCELLED")
public void onCancelStart(StateContext<OrderStatesEnum, OrderEventsEnum> context) {
log.info("【取消转换开始】开始取消订单");
String cancelReason = context.getMessageHeaders().get("cancelReason", String.class);
log.info("取消原因: {}", cancelReason);
log.info("验证取消权限...");
}
/**
* 取消订单转换结束时的处理
*/
@OnTransitionEnd(source = "UNPAID", target = "CANCELLED")
public void onCancelEnd() {
log.info("【取消转换结束】订单取消完成");
log.info("释放库存...");
log.info("发送取消通知...");
log.info("记录取消日志...");
}
/**
* 通用的状态转换处理(不区分开始和结束)
*/
@OnTransition
public void onAnyTransition() {
log.info("【通用转换】状态发生变化");
}
/**
* 从任意状态到指定状态的转换结束处理
*/
@OnTransitionEnd(target = "DONE")
public void onTransitionToDone() {
log.info("【到达完成状态】订单流程结束");
log.info("执行订单完成后的统计任务...");
log.info("更新用户积分...");
}
}</pre></div>
<p class="maodian"></p><h3>3.5 业务服务类</h3>
<p>在业务服务类中使用状态机:</p>
<p><strong>OrderService.java</strong></p>
<div class="jb51code"><pre class="brush:java;">import com.demo.enums.OrderStatesEnum;
import java.util.Map;
public interface OrderService {
/**
* 处理支付
*/
void payOrder(String orderId, Map<String, Object> paymentData);
/**
* 确认收货
*/
void confirmReceive(String orderId);
/**
* 取消订单
*/
void cancelOrder(String orderId, String reason);
/**
* 获取当前状态
*/
OrderStatesEnum getCurrentState();
}</pre></div>
<p><strong>OrderServiceImpl.java</strong></p>
<div class="jb51code"><pre class="brush:java;">import com.demo.enums.OrderEventsEnum;
import com.demo.enums.OrderStatesEnum;
import com.demo.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.state.State;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private StateMachine<OrderStatesEnum, OrderEventsEnum> stateMachine;
@Override
public void payOrder(String orderId, Map<String, Object> paymentData) {
log.info("开始处理订单任务状态事件,orderId: {},paymentData:{}", orderId, paymentData);
// 设置消息头,传递业务数据
Map<String, Object> headers = new HashMap<>();
headers.put("orderId", orderId);
headers.put("paymentData", paymentData);
headers.put("startTime", System.currentTimeMillis());
sendStateMachineEvent(OrderStatesEnum.UNPAID, OrderEventsEnum.PAY, headers);
}
@Override
public void confirmReceive(String orderId) {
log.info("开始处理确认收货状态事件,orderId: {}", orderId);
Map<String, Object> headers = new HashMap<>();
headers.put("orderId", orderId);
sendStateMachineEvent(OrderStatesEnum.WAITING_FOR_RECEIVE, OrderEventsEnum.RECEIVE, headers);
}
@Override
public void cancelOrder(String orderId, String reason) {
log.info("开始处理取消订单状态事件,orderId: {},reason:{}", orderId, reason);
Map<String, Object> headers = new HashMap<>();
headers.put("orderId", orderId);
headers.put("cancelReason", reason);
sendStateMachineEvent(OrderStatesEnum.UNPAID, OrderEventsEnum.CANCEL, headers);
}
/**
* 发送状态机事件的通用方法
* @param currentState 当前状态
* @param event 要发送的事件
* @param headers 消息头数据
*/
private void sendStateMachineEvent(OrderStatesEnum currentState, OrderEventsEnum event, Map<String, Object> headers) {
// 启动状态机
stateMachine.start();
// 根据当前任务状态设置状态机状态
log.info("当前任务状态: {}", currentState);
// 将状态机的状态设置为业务对象的实际状态
stateMachine.getStateMachineAccessor().doWithAllRegions(accessor -> {
accessor.resetStateMachine(new org.springframework.statemachine.support.DefaultStateMachineContext<>(
currentState, null, null, null));
});
// 构建并发送消息
Message<OrderEventsEnum> message = MessageBuilder
.withPayload(event)
.copyHeaders(headers)
.build();
stateMachine.sendEvent(message);
}
@Override
public OrderStatesEnum getCurrentState() {
State<OrderStatesEnum, OrderEventsEnum> state = stateMachine.getState();
return state == null ? null : state.getId();
}
}</pre></div>
<p class="maodian"></p><h3>3.6 控制器类</h3>
<p>提供 REST API 接口:</p>
<p><strong>OrderController.java</strong></p>
<div class="jb51code"><pre class="brush:java;">import com.demo.common.Result;
import com.demo.enums.OrderStatesEnum;
import com.demo.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* <p> @Title DemoController
* <p> @Description 测试Controller
*
* @author ACGkaka
* @date 2023/4/24 18:02
*/
@Slf4j
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/{orderId}/pay")
public Result<Object> payOrder(@PathVariable String orderId,
@RequestBody Map<String, Object> paymentData) {
try {
orderService.payOrder(orderId, paymentData);
return Result.succeed("支付处理中");
} catch (Exception e) {
log.error(e.getMessage(), e);
return Result.failed("支付失败:" + e.getMessage());
}
}
@PostMapping("/{orderId}/receive")
public Result<Object> receiveOrder(@PathVariable String orderId) {
try {
orderService.confirmReceive(orderId);
return Result.succeed("收货确认处理中");
} catch (Exception e) {
log.error(e.getMessage(), e);
return Result.failed("收货确认失败:" + e.getMessage());
}
}
@PostMapping("/{orderId}/cancel")
public Result<Object> cancelOrder(@PathVariable String orderId,
@RequestParam String reason) {
try {
orderService.cancelOrder(orderId, reason);
return Result.succeed("取消订单处理中");
} catch (Exception e) {
log.error(e.getMessage(), e);
return Result.failed("取消订单失败:" + e.getMessage());
}
}
@GetMapping("/{orderId}/status")
public Result<OrderStatesEnum> getOrderStatus() {
try {
Result<OrderStatesEnum> result = new Result<>();
OrderStatesEnum currentState = orderService.getCurrentState();
return result.setData(currentState);
} catch (Exception e) {
log.error(e.getMessage(), e);
return Result.failed("获取订单状态失败:" + e.getMessage());
}
}
}</pre></div>
<p class="maodian"></p><h3>3.7 测试结果</h3>
<p><strong>测试1:订单-支付订单接口</strong></p>
<ul><li>请求地址:http://localhost:8080/orders/1/pay</li><li>请求截图:</li></ul>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011014464381.png" /></p>
<ul><li>日志打印:</li></ul>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011014464319.jpg" /></p>
<p><strong>测试2:订单-收货确认接口</strong></p>
<ul><li>请求地址:http://localhost:8080/orders/1/receive</li><li>请求截图:</li></ul>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011014464387.png" /></p>
<ul><li>日志打印:</li></ul>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011014464365.png" /></p>
<p><strong>测试3:订单-取消订单接口</strong></p>
<ul><li>请求地址:http://localhost:8080/orders/1/cancel?reason=收货地址填写有误</li><li>请求截图:</li></ul>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011014464398.png" /></p>
<ul><li>日志打印:</li></ul>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011014464319.jpg" /></p>
<p><strong>测试4:查询状态接口</strong></p>
<ul><li>请求地址:http://localhost:8080/orders/1/status</li><li>请求截图:</li></ul>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011014464324.png" /></p>
<p><strong>问题点:所有订单共享一个状态</strong></p>
<p>虽然已经完成了状态机的基础操作,但是这里会发现一个问题:整个状态机只有一个状态。也就是说不管是哪个订单的状态都是一样的,那么有一个订单的状态为 DONE 的话,其余所有订单都走不了流程了,只能重启程序才能还原。</p>
<p>这肯定不行,所以就需要 <strong>将状态进行持久化,根据订单编号分别保存各自的状态</strong>。</p>
<p class="maodian"></p><h2>四、升级:状态机持久化</h2>
<p>使用 spring-statemachine 状态机持久化时,可以通过内存、spring-statemachine-redis 或 spring-statemachine-data-jpa 现有方式进行持久化处理。</p>
<p>因项目状态变化操作记录频繁,数据量大,使用 内存 或 spring-statemachine-redis 模式不可取,而项目使用的是 MyBatis,使用 spring-statemachine-data-jpa 也不合适,需要自定义实现。</p>
<p>项目结构如下:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011014464388.jpg" /></p>
<p class="maodian"></p><h3>4.1 Maven 依赖</h3>
<div class="jb51code"><pre class="brush:plain;"><!-- Statemachine -->
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-starter</artifactId>
<version>2.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-kryo</artifactId>
<version>2.2.3.RELEASE</version>
</dependency></pre></div>
<p class="maodian"></p><h3>4.2 自定义持久化类</h3>
<p><strong>CustomStateMachinePersist.java</strong></p>
<div class="jb51code"><pre class="brush:java;">import com.demo.common.redis.util.RedisUtil;
import com.demo.domain.OrderInfo;
import com.demo.enums.OrderEventsEnum;
import com.demo.enums.OrderStatesEnum;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.MessageHeaders;
import org.springframework.statemachine.StateMachineContext;
import org.springframework.statemachine.StateMachinePersist;
import org.springframework.statemachine.kryo.MessageHeadersSerializer;
import org.springframework.statemachine.kryo.StateMachineContextSerializer;
import org.springframework.statemachine.kryo.UUIDSerializer;
import org.springframework.statemachine.persist.DefaultStateMachinePersister;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.UUID;
/**
* 自定义状态机持久化
*/
@Slf4j
@Configuration
public class CustomStateMachinePersist {
private static final String REDIS_KEY_PREFIX = "ORDER_STATE_V1_";
@Autowired
private RedisUtil redisUtil;
private static final ThreadLocal<Kryo> KRYO_THREAD_LOCAL = ThreadLocal.withInitial(() -> {
Kryo kryo = new Kryo();
kryo.addDefaultSerializer(StateMachineContext.class, new StateMachineContextSerializer<>());
kryo.addDefaultSerializer(MessageHeaders.class, new MessageHeadersSerializer());
kryo.addDefaultSerializer(UUID.class, new UUIDSerializer());
// 设置引用追踪策略
kryo.setReferences(true);
return kryo;
});
private <S, E> byte[] serialize(StateMachineContext<S, E> context) {
Kryo kryo = KRYO_THREAD_LOCAL.get();
// 重置引用状态,避免状态污染
kryo.reset();
ByteArrayOutputStream out = new ByteArrayOutputStream();
Output output = new Output(out);
try {
kryo.writeObject(output, context);
output.flush();
return out.toByteArray();
} catch (Exception e) {
log.error("序列化状态机上下文失败", e);
throw new RuntimeException("序列化失败", e);
} finally {
output.close();
}
}
private <S, E> StateMachineContext<S, E> deserialize(byte[] data) {
if (data == null || data.length == 0) {
log.info("反序列化数据为空");
return null;
}
Kryo kryo = KRYO_THREAD_LOCAL.get();
// 重置引用状态,避免状态污染
kryo.reset();
ByteArrayInputStream in = new ByteArrayInputStream(data);
Input input = new Input(in);
try {
return kryo.readObject(input, StateMachineContext.class);
} catch (IndexOutOfBoundsException e) {
log.error("反序列化失败,可能是数据损坏或版本不兼容. 数据长度: {}, 错误: {}",
data.length, e.getMessage());
throw new RuntimeException("反序列化失败", e);
} catch (Exception e) {
log.error("反序列化状态机上下文时发生未知错误", e);
throw new RuntimeException("反序列化失败", e);
} finally {
input.close();
}
}
/**
* 状态机持久化
*/
@Bean
public DefaultStateMachinePersister<OrderStatesEnum, OrderEventsEnum, OrderInfo> stateMachinePersister(){
return new DefaultStateMachinePersister<>(new StateMachinePersist<OrderStatesEnum, OrderEventsEnum, OrderInfo>() {
@Override
public void write(StateMachineContext<OrderStatesEnum, OrderEventsEnum> context, OrderInfo info) throws Exception {
String key = REDIS_KEY_PREFIX + info.getId();
try {
byte[] value = serialize(context);
log.info("正在写入任务 {} 的状态机上下文,状态为 {}", info.getId(), context.getState());
redisUtil.set(key, value);
log.info("任务 {} 的状态机上下文已成功写入Redis", info.getId());
} catch (Exception e) {
log.error("写入任务 {} 的状态机上下文失败", info.getId(), e);
throw e;
}
}
@Override
public StateMachineContext<OrderStatesEnum, OrderEventsEnum> read(OrderInfo info) throws Exception {
String key = REDIS_KEY_PREFIX + info.getId();
log.info("正在读取任务 {} 的状态机上下文", info.getId());
try {
byte[] value = (byte[]) redisUtil.get(key);
if (value == null) {
log.info("未找到任务 {} 的状态机上下文", info.getId());
return null;
}
StateMachineContext<OrderStatesEnum, OrderEventsEnum> context = deserialize(value);
if (context == null) {
log.error("反序列化任务 {} 的状态机上下文失败,可能需要重新初始化", info.getId());
// 清理损坏的数据
redisUtil.delete(key);
return null;
}
log.info("已从Redis读取任务 {} 的状态机上下文,状态为 {}", info.getId(), context.getState());
return context;
} catch (Exception e) {
log.error("读取任务 {} 的状态机上下文时发生错误", info.getId(), e);
throw e;
}
}
});
}
}</pre></div>
<p class="maodian"></p><h3>4.3 编写状态机工具类</h3>
<p><strong>CustomStateMachineUtil.java</strong></p>
<div class="jb51code"><pre class="brush:java;">import com.demo.enums.OrderStatesEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.persist.StateMachinePersister;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@Slf4j
@Component
public class CustomStateMachineUtil<S, E, T> {
@Resource
private StateMachine<S, E> orderStateMachine;
@Resource
private StateMachinePersister<S, E, T> orderStateMachinePersister;
/**
* 发送状态机事件的通用方法
* @param currentState 当前状态
* @param event 要发送的事件
* @param info 消息实体
*/
public synchronized void sendEvent(S currentState, E event, T info) {
log.info("开始处理状态机事件: {}", info);
try {
// 启动状态机
orderStateMachine.start();
// 设置消息头,传递业务数据
Map<String, Object> headers = new HashMap<>();
headers.put("info", info);
headers.put("startTime", System.currentTimeMillis());
// 根据当前状态设置状态机状态
log.info("当前状态: {}", currentState);
// 方式一:从Redis恢复状态机状态
orderStateMachinePersister.restore(orderStateMachine, info);
// 方式二:将状态机的状态设置为业务对象的实际状态
// stateMachine.getStateMachineAccessor().doWithAllRegions(accessor -> {
// accessor.resetStateMachine(new org.springframework.statemachine.support.DefaultStateMachineContext<>(
// currentState, null, null, null));
// });
// 构建并发送消息
Message<E> message = MessageBuilder
.withPayload(event)
.copyHeaders(headers)
.build();
orderStateMachine.sendEvent(message);
// 持久化状态机状态
boolean persistSuccess = persist(info);
if (!persistSuccess) {
throw new RuntimeException("状态机持久化状态失败");
}
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
log.error("状态机发送事件失败. 事件: {}, 异常: {}", info, e.getMessage(), e);
throw new RuntimeException("状态机发送事件失败");
} finally {
if (Objects.nonNull(info)) {
log.info("当前状态: {}", currentState);
if (Arrays.asList(OrderStatesEnum.DONE, OrderStatesEnum.CANCELLED).contains(currentState)) {
log.info("已完成或已取消,停止状态机");
orderStateMachine.stop();
}
}
}
}
/**
* 持久化状态机状态
* @param info实体
* @return 是否持久化成功
*/
public synchronized boolean persist(T info) {
try {
log.info("持久化状态机开始,此时状态: {}", orderStateMachine.getState().getId());
// 启用持久化:将状态机状态持久化到Redis
log.info("持久化已启用,将状态机状态持久化到Redis");
orderStateMachinePersister.persist(orderStateMachine, info);
// 不启用持久化:跳过持久化步骤
// log.info("持久化未启用,跳过持久化步骤");
return true;
} catch (Exception e) {
log.error("持久化状态机状态失败. 异常: {}", e.getMessage(), e);
return false;
}
}
}</pre></div>
<p class="maodian"></p><h3>4.4 修改业务调用</h3>
<p><strong>OrderServiceImpl.java</strong></p>
<div class="jb51code"><pre class="brush:java;">import com.demo.common.stashmachine.util.CustomStateMachineUtil;
import com.demo.domain.OrderInfo;
import com.demo.enums.OrderEventsEnum;
import com.demo.enums.OrderStatesEnum;
import com.demo.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.persist.StateMachinePersister;
import org.springframework.statemachine.state.State;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private StateMachine<OrderStatesEnum, OrderEventsEnum> orderStateMachine;
@Autowired
private StateMachinePersister<OrderStatesEnum, OrderEventsEnum, OrderInfo> orderStateMachinePersister;
@Autowired
private CustomStateMachineUtil<OrderStatesEnum, OrderEventsEnum, OrderInfo> customStateMachineUtil;
@Override
public void payOrder(String orderId) {
log.info("开始处理订单任务状态事件,orderId: {}", orderId);
// 获取订单信息
OrderInfo orderInfo = new OrderInfo();
orderInfo.setId(orderId);
orderInfo.setState(OrderStatesEnum.UNPAID);
customStateMachineUtil.sendEvent(orderInfo.getState(), OrderEventsEnum.PAY, orderInfo);
}
@Override
public void confirmReceive(String orderId) {
log.info("开始处理确认收货状态事件,orderId: {}", orderId);
// 获取订单信息
OrderInfo orderInfo = new OrderInfo();
orderInfo.setId(orderId);
orderInfo.setState(OrderStatesEnum.WAITING_FOR_RECEIVE);
customStateMachineUtil.sendEvent(orderInfo.getState(), OrderEventsEnum.RECEIVE, orderInfo);
}
@Override
public void cancelOrder(String orderId, String reason) {
log.info("开始处理取消订单状态事件,orderId: {},reason:{}", orderId, reason);
// 获取订单信息
OrderInfo orderInfo = new OrderInfo();
orderInfo.setId(orderId);
orderInfo.setState(OrderStatesEnum.UNPAID);
orderInfo.setReason(reason);
customStateMachineUtil.sendEvent(orderInfo.getState(), OrderEventsEnum.CANCEL, orderInfo);
}
@Override
public OrderStatesEnum getCurrentState(String orderId) throws Exception {
OrderInfo info = new OrderInfo();
info.setId(orderId);
// 从Redis恢复状态机状态
orderStateMachinePersister.restore(orderStateMachine, info);
State<OrderStatesEnum, OrderEventsEnum> state = orderStateMachine.getState();
return state == null ? null : state.getId();
}
}</pre></div>
<p class="maodian"></p><h3>4.5 测试结果</h3>
<p>这次升级之后,再次调用接口可以发现,不同的订单编号已经可以分别存储不同的状态了,Redis 缓存内容如下:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011014464314.png" /></p>
<p>整理完毕,完结撒花~🌻</p>
<p>参考地址:</p>
<p>1.SpringBoot集成spring-statemachine状态机实现业务流程,https://blog.csdn.net/weixin_37598243/article/details/140907763</p>
<p>2.spring-statemachine 状态机自定义持久化入库,https://blog.csdn.net/sjy_2010/article/details/133862831</p>
<p>3.SpringBoot集成Spring Statemachine(状态机)完整示例,https://juejin.cn/post/7441760738458779684</p>
<p>到此这篇关于SpringBoot 集成 Statemachine的实战指南的文章就介绍到这了,更多相关SpringBoot 集成 Statemachine内容请搜索琼殿技术社区以前的文章或继续浏览下面的相关文章希望大家以后多多支持琼殿技术社区!</p>
<div class="art_xg">
<b>您可能感兴趣的文章:</b><ul><li>Spring StateMachine 使用小结</li><li>Spring Statemachine 状态机详解</li><li>Spring状态机 Statemachine使用小结</li><li>Spring StateMachine实现状态机使用示例详解</li><li>深入探讨Spring Statemachine在Spring中实现状态机的过程</li><li>StateMachine 状态机机制深入解析</li></ul>
</div>
</div>
<!--endmain-->
頁:
[1]