業(yè)務(wù)流程場景的處理利器—Spring狀態(tài)機
在日常開發(fā)系統(tǒng)中經(jīng)常會涉及到一些狀態(tài)管理的場景,典型的如訂單場景,訂單從創(chuàng)建到最終完成或取消,通常會經(jīng)歷多個狀態(tài)的轉(zhuǎn)換,那么如何高效地管理這些狀態(tài)流轉(zhuǎn),并在系統(tǒng)中靈活地擴展狀態(tài)和行為呢?Spring狀態(tài)機可以很好的幫助解決我們的實際問題。
1、認識Spring狀態(tài)機
Spring狀態(tài)機(稱為Spring State Machine)是一種可以管理狀態(tài)、事件之間的關(guān)系,以及他們之間的轉(zhuǎn)換。這是一個專門為應(yīng)用程序中的狀態(tài)管理和狀態(tài)轉(zhuǎn)換提供支持的框架。它簡化了事物對象在不同狀態(tài)下,不同事件轉(zhuǎn)化的代碼管理,讓其代碼變得更加清晰明了。
在我們的日常生活中電梯是我們非常熟悉的老伙計,電梯有停止、運行、開門、關(guān)門等狀態(tài)。如果我們按了某一層按了電梯按鈕(事件),電梯會進入運行狀態(tài),并且運行到目標樓層,然后停止并開門等一系列的動作。類比電梯的運行,Spring的狀態(tài)機也是在有限個狀態(tài)以及這些狀態(tài)之間的轉(zhuǎn)移和動作等行為,Spring狀態(tài)機的核心概念:
(1)狀態(tài) (State):定義系統(tǒng)可以存在的各個狀態(tài),如電梯的不同階段(停止、運行、開門、關(guān)門)。
(2)事件(Event):觸發(fā)狀態(tài)變遷的動作或條件,如用戶點擊“5f”按鈕觸發(fā)電梯的運行事件。
(3)轉(zhuǎn)換(Transition):定義了在特定狀態(tài)下接收到特定事件時,如何從一個狀態(tài)遷移到另一個狀態(tài)。
(4)動作(Action):可以在狀態(tài)變遷前后執(zhí)行的操作,如狀態(tài)切換時更新數(shù)據(jù)庫記錄、發(fā)送通知郵件等。
Spring狀態(tài)機的核心在于狀態(tài)變遷和事件驅(qū)動,它強調(diào)的是系統(tǒng)當前所處的狀態(tài),并且關(guān)注于系統(tǒng)如何根據(jù)接收到的外部事件或內(nèi)部條件進行狀態(tài)轉(zhuǎn)變。
2、實戰(zhàn)Spring狀態(tài)機
我們以典型的電商場景下的訂單狀態(tài)為例,介紹Spring狀態(tài)機如何實現(xiàn)訂單狀態(tài)的高效管理,訂單的狀態(tài)流轉(zhuǎn)圖如下所示:
圖片
項目的基礎(chǔ)結(jié)構(gòu)圖如下所示:
圖片
(1)添加的spring狀態(tài)機依賴
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-starter</artifactId>
<version>2.5.1</version>
</dependenc(2)定義訂單的狀態(tài)
@Getter
@AllArgsConstructor
public enum OrderStatusEnum {
WAIT_PAY(0, "待支付"),
WAIT_DELIVER(1, "待發(fā)貨"),
WAIT_RECEIVE(2, "待收貨"),
RECEIVED(3, "已收貨");
private Integer code;
private String msg;
}(3)定義訂單的事件
@Getter
@AllArgsConstructor
public enum OrderStatusEventEnum {
ORDER(1, "用戶下單"),
PAY(2, "用戶支付"),
DELIVER(3, "倉庫發(fā)貨"),
RECEIVE(4, "用戶收貨")
;
private final Integer code;
private final String msg;
}(4)定義訂單狀態(tài)的流轉(zhuǎn)和初始化
@Configurable
@EnableStateMachine(name ="orderStateMachine") //name的作用是給狀態(tài)機制命名,用于區(qū)分不同的狀態(tài)機
public class OrderStateMachineConfig extends StateMachineConfigurerAdapter<OrderStatusEnum, OrderStatusEventEnum> {
/**
* 初始化
*
*/
@Override
public void configure(StateMachineStateConfigurer<OrderStatusEnum, OrderStatusEventEnum> states) throws Exception {
states.withStates()
.initial(OrderStatusEnum.WAIT_PAY) //初始化訂單的狀態(tài)
.states(EnumSet.allOf(OrderStatusEnum.class)); //列舉訂單所有的預(yù)設(shè)狀態(tài)
}
/**
* 配置訂單狀態(tài)的流轉(zhuǎn)和觸發(fā)事件
*
*/
@Override
public void configure(StateMachineTransitionConfigurer<OrderStatusEnum, OrderStatusEventEnum> transitions) throws Exception {
transitions
// 通過支付事件 將訂單從待支付 -> 待發(fā)貨狀態(tài)(支付成功)
.withExternal().source(OrderStatusEnum.WAIT_PAY).target(OrderStatusEnum.WAIT_DELIVER).event(OrderStatusEventEnum.PAY)
.and()
// 通過發(fā)貨事件 已支將訂單從待發(fā)貨 -> 待收貨狀態(tài)
.withExternal().source(OrderStatusEnum.WAIT_DELIVER).target(OrderStatusEnum.WAIT_RECEIVE).event(OrderStatusEventEnum.DELIVER)
.and()
// 通過用戶接收事件 將訂單從待收貨 -> 已收貨狀態(tài)
.withExternal().source(OrderStatusEnum.WAIT_RECEIVE).target(OrderStatusEnum.RECEIVED).event(OrderStatusEventEnum.RECEIVE);
}
}通過上面的配置就可以實現(xiàn)訂單狀態(tài)直接的轉(zhuǎn)換,主要用于說明訂單的狀態(tài)和事件的綁定關(guān)系,并且指明了訂單狀態(tài)如何流轉(zhuǎn)的(原始狀態(tài)是什么,目標的狀態(tài)是什么 ,通過什么事件觸發(fā))。
(5)定義監(jiān)聽
訂單的節(jié)點已經(jīng)定義,那么每個節(jié)點是如何工作的呢?此時需要配置監(jiān)聽,用于實現(xiàn)當狀態(tài)發(fā)生變化的時候需要完成什么動作。
@Slf4j
@Component
@WithStateMachine(name = "orderStateMachine")
public class OrderMachineListener {
@OnTransition(source = "WAIT_PAY", target = "WAIT_DELIVER")
public void pay(Message<OrderStatusEventEnum> message) {
// 獲取消息頭中的order對象
OrderEntity entity = message.getHeaders().get("order", OrderEntity.class);
entity.setStatus(OrderStatusEnum.WAIT_DELIVER.getCode());
System.out.println("orderMachine WAIT_PAY----> WAIT_DELIVER");
}
@OnTransition(source = "WAIT_DELIVER", target = "WAIT_RECEIVE")
public void delivery(Message<OrderStatusEventEnum> message) {
// 獲取消息頭中的order對象
OrderEntity entity = message.getHeaders().get("order", OrderEntity.class);
entity.setStatus(OrderStatusEnum.WAIT_DELIVER.getCode());
System.out.println("orderMachine WAIT_DELIVER----> WAIT_RECEIVE");
}
@OnTransition(source = "WAIT_RECEIVE", target = "RECEIVED")
public void received(Message<OrderStatusEventEnum> message) {
// 獲取消息頭中的order對象
OrderEntity entity = message.getHeaders().get("order", OrderEntity.class);
entity.setStatus(OrderStatusEnum.WAIT_DELIVER.getCode());
System.out.println("orderMachine WAIT_RECEIVE----> RECEIVED");
}
}(6)觸發(fā)狀態(tài)機的工作
訂單狀態(tài)機整個生命周期的狀態(tài)定義完成、訂單的狀態(tài)發(fā)生了轉(zhuǎn)換后需要執(zhí)行的行為也定義完成,那么如何觸發(fā)整個鏈路工作呢?此時就需要加入觸發(fā)的入口:
@Component
@Slf4j
public class OrderProcessorService {
@Resource
private StateMachine<OrderStatusEnum, OrderStatusEventEnum> orderStatusMachine;
public Boolean process(OrderEntity order, OrderStatusEventEnum event) {
orderStatusMachine.start();
//訂單的對象封裝到message中
Message<OrderStatusEventEnum> message = MessageBuilder.withPayload(event).
setHeader("order",order).
build();
//使用狀態(tài)機發(fā)送這個消息
return this.touchEvent(message);
}
private boolean touchEvent(Message<OrderStatusEventEnum> message) {
OrderEntity order = (OrderEntity)message.getHeaders().get("order");
System.out.println("訂單的信息 orderId=" + (Objects.nonNull(order) ? order.getId() : "-"));
return orderStatusMachine.sendEvent(message);
}
}sendEvent(message)是十分關(guān)鍵的,因為在這里只要輸入什么樣的事件,那么它就按照預(yù)先設(shè)定的規(guī)則觸發(fā)狀態(tài)的流轉(zhuǎn),并執(zhí)行監(jiān)聽中方法。
(7)測試Conrtoller
@RestController
@RequestMapping("/order")
public class OrderController {
@Resource
private OrderProcessorService orderProcessorService;
@GetMapping("/pay")
public String pay(Integer orderId) {
OrderEntity order = new OrderEntity();
order.setId(orderId);
order.setStatus(OrderStatusEnum.WAIT_PAY.getCode());
orderProcessorService.process(order, OrderStatusEventEnum.PAY);
return "pay success";
}
@GetMapping("/deliver")
public String deliver(Integer orderId) {
OrderEntity order = new OrderEntity();
order.setId(orderId);
order.setStatus(OrderStatusEnum.WAIT_DELIVER.getCode());
orderProcessorService.process(order, OrderStatusEventEnum.DELIVER);
return "deliver success";
}
@GetMapping("/receive")
public String receive(Integer orderId) {
OrderEntity order = new OrderEntity();
order.setId(orderId);
order.setStatus(OrderStatusEnum.WAIT_RECEIVE.getCode());
orderProcessorService.process(order, OrderStatusEventEnum.RECEIVE);
return "receive success";
}
}(8)執(zhí)行的結(jié)果
(a)支付的狀態(tài)流轉(zhuǎn)
圖片
控制臺的輸出結(jié)果:
圖片
(b)發(fā)貨的狀態(tài)流轉(zhuǎn)
圖片
控制臺的輸出結(jié)果:
圖片
(c)收貨狀態(tài)的流轉(zhuǎn)
圖片
控制臺的輸出結(jié)果:
圖片
至此,我們實現(xiàn)使用Spring狀態(tài)機實現(xiàn)了訂單的狀態(tài)流轉(zhuǎn)和觸發(fā)對應(yīng)的事件。
總結(jié)
(1)如果業(yè)務(wù)流程復(fù)雜,狀態(tài)多,狀態(tài)切換頻繁,Spring狀態(tài)機就非常的適用,它可以提升代碼的維護性和擴展性。
(2)Spring狀態(tài)機可以直接集成到現(xiàn)有的服務(wù)中,適合高并發(fā)低延遲的場景,涉及到單個流程的流轉(zhuǎn)業(yè)務(wù),可以考慮使用狀態(tài)機。


























