SpringBoot 集成 Statemachine的實戰(zhàn)指南

- 官網(wǎng)地址:https://spring.io/projects/spring-statemachine#learn
- 官方文檔:https://docs.spring.io/spring-statemachine/docs/3.2.1/reference/#statemachine-getting-started
一、Statemachine 簡介
Spring Statemachine 是一個由 Spring 團(tuán)隊提供的 輕量級狀態(tài)機框架,它允許開發(fā)者以簡便且強大的方式管理復(fù)雜的狀態(tài)流轉(zhuǎn)邏輯。該框架建立在 有限狀態(tài)機(FSM) 的概念之上,提供了一種簡潔且靈活的方式來定義、管理和執(zhí)行狀態(tài)機。
1.1 核心概念
- 狀態(tài)(State):系統(tǒng)可能處于的不同條件或模式,是狀態(tài)機的核心組成單元。
- 事件(Event):觸發(fā)狀態(tài)轉(zhuǎn)換的動作或消息,是引起狀態(tài)機從當(dāng)前狀態(tài)遷移到新狀態(tài)的原因。
- 轉(zhuǎn)換(Transition):描述了在何種條件下,當(dāng)接收到特定事件時,系統(tǒng)可以從一個狀態(tài)轉(zhuǎn)移到另一個狀態(tài)。
- 動作(Action):在狀態(tài)轉(zhuǎn)換時執(zhí)行的具體操作。
1.2 主要特性
Spring Statemachine 提供了豐富的功能特性:
- 易于使用的平面(一級)狀態(tài)機,適用于簡單的用例;
- 分層狀態(tài)結(jié)構(gòu),以簡化復(fù)雜的狀態(tài)配置;
- 狀態(tài)機區(qū)域提供更復(fù)雜的狀態(tài)配置;
- 觸發(fā)器、轉(zhuǎn)換、守衛(wèi)和動作的使用;
- 類型安全的配置適配器;
- 狀態(tài)機事件監(jiān)聽器;
- Spring IoC 集成將 Bean 與 狀態(tài)機 相關(guān)聯(lián)。
1.3 注解驅(qū)動開發(fā)方式
為了簡化開發(fā),可以使用 Statemachine 的 注解驅(qū)動開發(fā)方式,特別是 @OnTransition、@OnTransitionStart、@OnTransitionEnd 和 @WithStateMachine 注解的使用,這些注解能夠讓我們以更加 聲明式 和 簡介 的方式處理狀態(tài)轉(zhuǎn)換邏輯。
注解驅(qū)動的開發(fā)方式具有以下優(yōu)勢:
- 代碼簡潔性:將狀態(tài)轉(zhuǎn)換邏輯直接注解在方法上,減少模板代碼;
- 關(guān)注點分離:業(yè)務(wù)邏輯與狀態(tài)機配置清晰分離,提高可維護(hù)性;
- 類型安全:編譯時檢查注解的正確性,減少運行時錯誤;
- 可讀性強:通過注解直觀地表達(dá)狀態(tài)轉(zhuǎn)換的意圖。
1.4 核心注解詳解
1)@WithStateMachine 注解
@WithStateMachine 注解用于標(biāo)識一個類是與狀態(tài)機相關(guān)的監(jiān)聽器,它告訴 Spring 這個類中的方法需要接受狀態(tài)機的事件通知。
- 使用場景:標(biāo)記狀態(tài)機監(jiān)聽器類,是類中的狀態(tài)轉(zhuǎn)換注解生效。
2)@OnTransition 注解
@OnTrasition 注解用于標(biāo)記在狀態(tài)轉(zhuǎn)換發(fā)生時執(zhí)行的方法,它不區(qū)分轉(zhuǎn)換的開始和結(jié)束。
- 使用場景:當(dāng)不關(guān)心轉(zhuǎn)換的具體階段,只需要在轉(zhuǎn)換發(fā)生時執(zhí)行某些邏輯時使用。
3)@OnTransitionStart 注解
@OnTransitionStart 注解用于標(biāo)記在狀態(tài)轉(zhuǎn)換開始時執(zhí)行的方法。
- 使用場景:需要在狀態(tài)轉(zhuǎn)換剛開始時執(zhí)行與處理邏輯,如參數(shù)驗證、資源準(zhǔn)備等。
4)@OnTransitionEnd 注解
@OnTransitionEnd 注解用于標(biāo)記在狀態(tài)轉(zhuǎn)換 結(jié)束時 執(zhí)行的方法。
- 使用場景:需要在狀態(tài)轉(zhuǎn)換完成后執(zhí)行清理邏輯、記錄日志、發(fā)送通知等。
1.5 包含的模塊
Spring Statemachine 包含的模塊如下:
| 模塊 | 描述 |
|---|---|
spring-statemachine-core | Spring Statemachine的核心系統(tǒng)。 |
spring-statemachine-recipes-common | 不需要核心框架之外的依賴項的常見配方。 |
spring-statemachine-kryo | KryoSpring Statemachine的序列化程序。 |
spring-statemachine-data-common | Spring Data的通用支持模塊。 |
spring-statemachine-data-jpa | 支持Spring Data JPA模塊。 |
spring-statemachine-data-redis | 支持Spring Data Redis模塊。 |
spring-statemachine-data-mongodb | 支持Spring Data MongoDB模塊。 |
spring-statemachine-zookeeper | 分布式狀態(tài)機的Zooeman集成。 |
spring-statemachine-test | 狀態(tài)機測試支持模塊。 |
spring-statemachine-cluster | Spring Cloud Cluster的支持模塊。請注意,Spring Cloud Cluster已被Spring Integration取代。 |
spring-statemachine-uml | 使用Eclipse Papyrus進(jìn)行UI UML建模的支持模塊。 |
spring-statemachine-autoconfigure | Spring Boot的支持模塊。 |
spring-statemachine-bom | 物料清單pom。 |
spring-statemachine-starter | 彈簧啟動啟動器。 |
二、知識回顧——狀態(tài)模式
2.1 什么是狀態(tài)模式?
狀態(tài)模式(State Pattern) 是一種 行為型 設(shè)計模式,對有狀態(tài)的對象,把復(fù)雜的 “判斷邏輯” 提取到不同的狀態(tài)對象中,允許狀態(tài)對象在其內(nèi)部狀態(tài)發(fā)生改變時,改變其行為。
2.2 狀態(tài)模式的優(yōu)缺點
狀態(tài)模式的優(yōu)點:
- 結(jié)構(gòu)清晰 :狀態(tài)模式將與特定狀態(tài)相關(guān)的行為局部化道一個狀態(tài)中,并且將不同狀態(tài)的行為分割開來,滿足 “但一職責(zé)原則”。
- 將狀態(tài)轉(zhuǎn)換顯示化:減少對象間的相互依賴,將不同的狀態(tài)引入獨立的對象中會是的狀態(tài)轉(zhuǎn)換變得更加明確,且減少對相見的相互依賴。
- 狀態(tài)類職責(zé)明確:有利于程序的擴(kuò)展。通過定義新的子類很容易地增加新的狀態(tài)和轉(zhuǎn)換。
狀態(tài)模式的缺點:
- 狀態(tài)模式的使用必然會增加系統(tǒng)的類與對象的個數(shù)。
- 狀態(tài)模式的結(jié)構(gòu)與實現(xiàn)都較為復(fù)雜,如果使用不當(dāng)會導(dǎo)致程序結(jié)構(gòu)和代碼的混亂。
- 狀態(tài)模式對開閉原則的支持并不太好,對于可以切換狀態(tài)的狀態(tài)模式,增加新的狀態(tài)類需要修改那些負(fù)責(zé)狀態(tài)轉(zhuǎn)換的源碼,否則無法切換到新增狀態(tài),而且修改某個狀態(tài)類的行為也需要修改對應(yīng)類的源碼。
2.3 狀態(tài)模式的實現(xiàn)結(jié)構(gòu)
狀態(tài)模式把受環(huán)境改變的對象行為包裝在不同的狀態(tài)對象里,其意圖是讓一個對象在其內(nèi)部狀態(tài)改變的時候,其行為也隨之改變。現(xiàn)在我們來分析其基本結(jié)構(gòu)和實現(xiàn)方法。
狀態(tài)模式主要包含三個角色:
- Context(環(huán)境類):定義客戶端感興趣的接口,維護(hù)一個 State 子類的實例,這個示例定義當(dāng)前狀態(tài)。
- State(抽象狀態(tài)類):定義一個接口,用以封裝 Context 的特定狀態(tài)相關(guān)的行為。
- ConcreteState(具體狀態(tài)類):每一個子類實現(xiàn)一個與 Context 的一個狀態(tài)相關(guān)的行為。
三、SpringBoot 集成
項目結(jié)構(gòu)如下:

3.1 Maven 依賴
對于 SpringBoot 2.x 項目,可以使用 2.x 版本的 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>3.2 定義狀態(tài)和事件枚舉
首先,我們需要定義狀態(tài)機和事件的所有可能值。以訂單系統(tǒng)為例:
OrderStatesEnum.java、OrderEventsEnum.java
public enum OrderStatesEnum {
UNPAID, // 待支付
WAITING_FOR_RECEIVE, // 待收貨
DONE, // 完成
CANCELLED // 取消
}
public enum OrderEventsEnum {
PAY, // 支付
RECEIVE, // 收貨
CANCEL // 取消
}3.3 配置狀態(tài)機
接下來,我們需要配置狀態(tài)機,定義狀態(tài)轉(zhuǎn)換規(guī)則:
StateMachineConfig.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);
}
}3.4 使用注解實現(xiàn)狀態(tài)監(jiān)聽器
這段代碼展示如何使用注解來監(jiān)聽狀態(tài)轉(zhuǎn)換:
OrderStateListener.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 {
/**
* 支付轉(zhuǎn)換開始時的處理
*/
@OnTransitionStart(source = "UNPAID", target = "WAITING_FOR_RECEIVE")
public void onPayStart(StateContext<OrderStatesEnum, OrderEventsEnum> context) {
log.info("【支付轉(zhuǎn)換開始】開始處理支付邏輯");
// 獲取轉(zhuǎn)換相關(guān)的數(shù)據(jù)
Object paymentData = context.getMessageHeader("paymentData");
if (paymentData != null) {
log.info("支付數(shù)據(jù):{}", paymentData);
}
// 執(zhí)行支付前的驗證邏輯
log.info("驗證支付參數(shù)...");
log.info("檢查庫存...");
log.info("預(yù)扣庫存...");
}
/**
* 支付轉(zhuǎn)換結(jié)束時的處理
*/
@OnTransitionEnd(source = "UNPAID", target = "WAITING_FOR_RECEIVE")
public void onPayEnd(StateContext<OrderStatesEnum, OrderEventsEnum> context) {
log.info("【支付轉(zhuǎn)換結(jié)束】支付處理完成");
// 執(zhí)行支付后的清理邏輯
log.info("更新庫存...");
log.info("生成支付憑證...");
log.info("發(fā)送支付成功通知...");
// 記錄轉(zhuǎn)換耗時
Long startTime = (Long) context.getMessageHeader("startTime");
if (startTime != null) {
long duration = System.currentTimeMillis() - startTime;
log.info("支付處理耗時:{}ms", duration);
}
}
/**
* 收貨轉(zhuǎn)換開始時的處理
*/
@OnTransitionStart(source = "WAITING_FOR_RECEIVE", target = "DONE")
public void onReceiveStart() {
log.info("【收貨轉(zhuǎn)換開始】開始確認(rèn)收貨");
log.info("驗證收貨權(quán)限...");
log.info("檢查物流信息...");
}
/**
* 收貨轉(zhuǎn)換結(jié)束時的處理
*/
@OnTransitionEnd(source = "WAITING_FOR_RECEIVE", target = "DONE")
public void onReceiveEnd() {
log.info("【收貨轉(zhuǎn)換結(jié)束】收貨確認(rèn)完成");
log.info("更新訂單完成時間...");
log.info("計算商家評分...");
log.info("發(fā)送訂單完成通知...");
}
/**
* 取消訂單轉(zhuǎn)換開始時的處理
*/
@OnTransitionStart(source = "UNPAID", target = "CANCELLED")
public void onCancelStart(StateContext<OrderStatesEnum, OrderEventsEnum> context) {
log.info("【取消轉(zhuǎn)換開始】開始取消訂單");
String cancelReason = context.getMessageHeaders().get("cancelReason", String.class);
log.info("取消原因: {}", cancelReason);
log.info("驗證取消權(quán)限...");
}
/**
* 取消訂單轉(zhuǎn)換結(jié)束時的處理
*/
@OnTransitionEnd(source = "UNPAID", target = "CANCELLED")
public void onCancelEnd() {
log.info("【取消轉(zhuǎn)換結(jié)束】訂單取消完成");
log.info("釋放庫存...");
log.info("發(fā)送取消通知...");
log.info("記錄取消日志...");
}
/**
* 通用的狀態(tài)轉(zhuǎn)換處理(不區(qū)分開始和結(jié)束)
*/
@OnTransition
public void onAnyTransition() {
log.info("【通用轉(zhuǎn)換】狀態(tài)發(fā)生變化");
}
/**
* 從任意狀態(tài)到指定狀態(tài)的轉(zhuǎn)換結(jié)束處理
*/
@OnTransitionEnd(target = "DONE")
public void onTransitionToDone() {
log.info("【到達(dá)完成狀態(tài)】訂單流程結(jié)束");
log.info("執(zhí)行訂單完成后的統(tǒng)計任務(wù)...");
log.info("更新用戶積分...");
}
}3.5 業(yè)務(wù)服務(wù)類
在業(yè)務(wù)服務(wù)類中使用狀態(tài)機:
OrderService.java
import com.demo.enums.OrderStatesEnum;
import java.util.Map;
public interface OrderService {
/**
* 處理支付
*/
void payOrder(String orderId, Map<String, Object> paymentData);
/**
* 確認(rèn)收貨
*/
void confirmReceive(String orderId);
/**
* 取消訂單
*/
void cancelOrder(String orderId, String reason);
/**
* 獲取當(dāng)前狀態(tài)
*/
OrderStatesEnum getCurrentState();
}OrderServiceImpl.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("開始處理訂單任務(wù)狀態(tài)事件,orderId: {},paymentData:{}", orderId, paymentData);
// 設(shè)置消息頭,傳遞業(yè)務(wù)數(shù)據(jù)
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("開始處理確認(rèn)收貨狀態(tài)事件,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("開始處理取消訂單狀態(tài)事件,orderId: {},reason:{}", orderId, reason);
Map<String, Object> headers = new HashMap<>();
headers.put("orderId", orderId);
headers.put("cancelReason", reason);
sendStateMachineEvent(OrderStatesEnum.UNPAID, OrderEventsEnum.CANCEL, headers);
}
/**
* 發(fā)送狀態(tài)機事件的通用方法
* @param currentState 當(dāng)前狀態(tài)
* @param event 要發(fā)送的事件
* @param headers 消息頭數(shù)據(jù)
*/
private void sendStateMachineEvent(OrderStatesEnum currentState, OrderEventsEnum event, Map<String, Object> headers) {
// 啟動狀態(tài)機
stateMachine.start();
// 根據(jù)當(dāng)前任務(wù)狀態(tài)設(shè)置狀態(tài)機狀態(tài)
log.info("當(dāng)前任務(wù)狀態(tài): {}", currentState);
// 將狀態(tài)機的狀態(tài)設(shè)置為業(yè)務(wù)對象的實際狀態(tài)
stateMachine.getStateMachineAccessor().doWithAllRegions(accessor -> {
accessor.resetStateMachine(new org.springframework.statemachine.support.DefaultStateMachineContext<>(
currentState, null, null, null));
});
// 構(gòu)建并發(fā)送消息
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();
}
}3.6 控制器類
提供 REST API 接口:
OrderController.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("收貨確認(rèn)處理中");
} catch (Exception e) {
log.error(e.getMessage(), e);
return Result.failed("收貨確認(rèn)失?。? + 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("獲取訂單狀態(tài)失?。? + e.getMessage());
}
}
}3.7 測試結(jié)果
測試1:訂單-支付訂單接口
- 請求地址:http://localhost:8080/orders/1/pay
- 請求截圖:

- 日志打?。?/li>

測試2:訂單-收貨確認(rèn)接口
- 請求地址:http://localhost:8080/orders/1/receive
- 請求截圖:

- 日志打印:

測試3:訂單-取消訂單接口

- 日志打?。?/li>

測試4:查詢狀態(tài)接口
- 請求地址:http://localhost:8080/orders/1/status
- 請求截圖:

問題點:所有訂單共享一個狀態(tài)
雖然已經(jīng)完成了狀態(tài)機的基礎(chǔ)操作,但是這里會發(fā)現(xiàn)一個問題:整個狀態(tài)機只有一個狀態(tài)。也就是說不管是哪個訂單的狀態(tài)都是一樣的,那么有一個訂單的狀態(tài)為 DONE 的話,其余所有訂單都走不了流程了,只能重啟程序才能還原。
這肯定不行,所以就需要 將狀態(tài)進(jìn)行持久化,根據(jù)訂單編號分別保存各自的狀態(tài)。
四、升級:狀態(tài)機持久化
使用 spring-statemachine 狀態(tài)機持久化時,可以通過內(nèi)存、spring-statemachine-redis 或 spring-statemachine-data-jpa 現(xiàn)有方式進(jìn)行持久化處理。
因項目狀態(tài)變化操作記錄頻繁,數(shù)據(jù)量大,使用 內(nèi)存 或 spring-statemachine-redis 模式不可取,而項目使用的是 MyBatis,使用 spring-statemachine-data-jpa 也不合適,需要自定義實現(xiàn)。
項目結(jié)構(gòu)如下:

4.1 Maven 依賴
<!-- 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>4.2 自定義持久化類
CustomStateMachinePersist.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;
/**
* 自定義狀態(tài)機持久化
*/
@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());
// 設(shè)置引用追蹤策略
kryo.setReferences(true);
return kryo;
});
private <S, E> byte[] serialize(StateMachineContext<S, E> context) {
Kryo kryo = KRYO_THREAD_LOCAL.get();
// 重置引用狀態(tài),避免狀態(tài)污染
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("序列化狀態(tài)機上下文失敗", 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("反序列化數(shù)據(jù)為空");
return null;
}
Kryo kryo = KRYO_THREAD_LOCAL.get();
// 重置引用狀態(tài),避免狀態(tài)污染
kryo.reset();
ByteArrayInputStream in = new ByteArrayInputStream(data);
Input input = new Input(in);
try {
return kryo.readObject(input, StateMachineContext.class);
} catch (IndexOutOfBoundsException e) {
log.error("反序列化失敗,可能是數(shù)據(jù)損壞或版本不兼容. 數(shù)據(jù)長度: {}, 錯誤: {}",
data.length, e.getMessage());
throw new RuntimeException("反序列化失敗", e);
} catch (Exception e) {
log.error("反序列化狀態(tài)機上下文時發(fā)生未知錯誤", e);
throw new RuntimeException("反序列化失敗", e);
} finally {
input.close();
}
}
/**
* 狀態(tài)機持久化
*/
@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("正在寫入任務(wù) {} 的狀態(tài)機上下文,狀態(tài)為 {}", info.getId(), context.getState());
redisUtil.set(key, value);
log.info("任務(wù) {} 的狀態(tài)機上下文已成功寫入Redis", info.getId());
} catch (Exception e) {
log.error("寫入任務(wù) {} 的狀態(tài)機上下文失敗", info.getId(), e);
throw e;
}
}
@Override
public StateMachineContext<OrderStatesEnum, OrderEventsEnum> read(OrderInfo info) throws Exception {
String key = REDIS_KEY_PREFIX + info.getId();
log.info("正在讀取任務(wù) {} 的狀態(tài)機上下文", info.getId());
try {
byte[] value = (byte[]) redisUtil.get(key);
if (value == null) {
log.info("未找到任務(wù) {} 的狀態(tài)機上下文", info.getId());
return null;
}
StateMachineContext<OrderStatesEnum, OrderEventsEnum> context = deserialize(value);
if (context == null) {
log.error("反序列化任務(wù) {} 的狀態(tài)機上下文失敗,可能需要重新初始化", info.getId());
// 清理損壞的數(shù)據(jù)
redisUtil.delete(key);
return null;
}
log.info("已從Redis讀取任務(wù) {} 的狀態(tài)機上下文,狀態(tài)為 {}", info.getId(), context.getState());
return context;
} catch (Exception e) {
log.error("讀取任務(wù) {} 的狀態(tài)機上下文時發(fā)生錯誤", info.getId(), e);
throw e;
}
}
});
}
}4.3 編寫狀態(tài)機工具類
CustomStateMachineUtil.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;
/**
* 發(fā)送狀態(tài)機事件的通用方法
* @param currentState 當(dāng)前狀態(tài)
* @param event 要發(fā)送的事件
* @param info 消息實體
*/
public synchronized void sendEvent(S currentState, E event, T info) {
log.info("開始處理狀態(tài)機事件: {}", info);
try {
// 啟動狀態(tài)機
orderStateMachine.start();
// 設(shè)置消息頭,傳遞業(yè)務(wù)數(shù)據(jù)
Map<String, Object> headers = new HashMap<>();
headers.put("info", info);
headers.put("startTime", System.currentTimeMillis());
// 根據(jù)當(dāng)前狀態(tài)設(shè)置狀態(tài)機狀態(tài)
log.info("當(dāng)前狀態(tài): {}", currentState);
// 方式一:從Redis恢復(fù)狀態(tài)機狀態(tài)
orderStateMachinePersister.restore(orderStateMachine, info);
// 方式二:將狀態(tài)機的狀態(tài)設(shè)置為業(yè)務(wù)對象的實際狀態(tài)
// stateMachine.getStateMachineAccessor().doWithAllRegions(accessor -> {
// accessor.resetStateMachine(new org.springframework.statemachine.support.DefaultStateMachineContext<>(
// currentState, null, null, null));
// });
// 構(gòu)建并發(fā)送消息
Message<E> message = MessageBuilder
.withPayload(event)
.copyHeaders(headers)
.build();
orderStateMachine.sendEvent(message);
// 持久化狀態(tài)機狀態(tài)
boolean persistSuccess = persist(info);
if (!persistSuccess) {
throw new RuntimeException("狀態(tài)機持久化狀態(tài)失敗");
}
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
log.error("狀態(tài)機發(fā)送事件失敗. 事件: {}, 異常: {}", info, e.getMessage(), e);
throw new RuntimeException("狀態(tài)機發(fā)送事件失敗");
} finally {
if (Objects.nonNull(info)) {
log.info("當(dāng)前狀態(tài): {}", currentState);
if (Arrays.asList(OrderStatesEnum.DONE, OrderStatesEnum.CANCELLED).contains(currentState)) {
log.info("已完成或已取消,停止?fàn)顟B(tài)機");
orderStateMachine.stop();
}
}
}
}
/**
* 持久化狀態(tài)機狀態(tài)
* @param info 實體
* @return 是否持久化成功
*/
public synchronized boolean persist(T info) {
try {
log.info("持久化狀態(tài)機開始,此時狀態(tài): {}", orderStateMachine.getState().getId());
// 啟用持久化:將狀態(tài)機狀態(tài)持久化到Redis
log.info("持久化已啟用,將狀態(tài)機狀態(tài)持久化到Redis");
orderStateMachinePersister.persist(orderStateMachine, info);
// 不啟用持久化:跳過持久化步驟
// log.info("持久化未啟用,跳過持久化步驟");
return true;
} catch (Exception e) {
log.error("持久化狀態(tài)機狀態(tài)失敗. 異常: {}", e.getMessage(), e);
return false;
}
}
}4.4 修改業(yè)務(wù)調(diào)用
OrderServiceImpl.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("開始處理訂單任務(wù)狀態(tài)事件,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("開始處理確認(rèn)收貨狀態(tài)事件,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("開始處理取消訂單狀態(tài)事件,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恢復(fù)狀態(tài)機狀態(tài)
orderStateMachinePersister.restore(orderStateMachine, info);
State<OrderStatesEnum, OrderEventsEnum> state = orderStateMachine.getState();
return state == null ? null : state.getId();
}
}4.5 測試結(jié)果
這次升級之后,再次調(diào)用接口可以發(fā)現(xiàn),不同的訂單編號已經(jīng)可以分別存儲不同的狀態(tài)了,Redis 緩存內(nèi)容如下:

整理完畢,完結(jié)撒花~??
參考地址:
1.SpringBoot集成spring-statemachine狀態(tài)機實現(xiàn)業(yè)務(wù)流程,https://blog.csdn.net/weixin_37598243/article/details/140907763
2.spring-statemachine 狀態(tài)機自定義持久化入庫,https://blog.csdn.net/sjy_2010/article/details/133862831
3.SpringBoot集成Spring Statemachine(狀態(tài)機)完整示例,https://juejin.cn/post/7441760738458779684
到此這篇關(guān)于SpringBoot 集成 Statemachine的實戰(zhàn)指南的文章就介紹到這了,更多相關(guān)SpringBoot 集成 Statemachine內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring Boot 快速搭建微服務(wù)框架詳細(xì)教程
SpringBoot是為了簡化Spring應(yīng)用的創(chuàng)建、運行、調(diào)試、部署等而出現(xiàn)的,使用它可以做到專注于Spring應(yīng)用的開發(fā),而無需過多關(guān)注XML的配置。本文重點給大家介紹Spring Boot 快速搭建微服務(wù)框架詳細(xì)教程,需要的的朋友參考下吧2017-09-09
java 服務(wù)器接口快速開發(fā)之servlet詳細(xì)教程
Servlet(Server Applet)是Java Servlet的簡稱,稱為小服務(wù)程序或服務(wù)連接器,用Java編寫的服務(wù)器端程序,具有獨立于平臺和協(xié)議的特性,主要功能在于交互式地瀏覽和生成數(shù)據(jù),生成動態(tài)Web內(nèi)容2021-06-06
Java微信公眾平臺開發(fā)(15) 微信JSSDK的使用
這篇文章主要為大家詳細(xì)介紹了Java微信公眾平臺開發(fā)第十五步,微信JSSDK的使用方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-04-04
Resty開發(fā)restful版本的Jfinal深入研究
這篇文章主要為大家介紹了Resty開發(fā)restful版本的Jfinal深入研究有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03
SpringBoot中的maven插件spring-boot-maven-plugin使用
這篇文章主要介紹了SpringBoot中的maven插件spring-boot-maven-plugin使用方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12
Spring使用Jackson實現(xiàn)轉(zhuǎn)換XML與Java對象
這篇文章主要為大家詳細(xì)介紹了Spring如何使用Jackson實現(xiàn)轉(zhuǎn)換XML與Java對象,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-02-02

