業(yè)務流程場景的處理利器—Spring狀態(tài)機
在日常開發(fā)系統中經常會涉及到一些狀態(tài)管理的場景,典型的如訂單場景,訂單從創(chuàng)建到最終完成或取消,通常會經歷多個狀態(tài)的轉換,那么如何高效地管理這些狀態(tài)流轉,并在系統中靈活地擴展狀態(tài)和行為呢?Spring狀態(tài)機可以很好的幫助解決我們的實際問題。
1、認識Spring狀態(tài)機
Spring狀態(tài)機(稱為Spring State Machine)是一種可以管理狀態(tài)、事件之間的關系,以及他們之間的轉換。這是一個專門為應用程序中的狀態(tài)管理和狀態(tài)轉換提供支持的框架。它簡化了事物對象在不同狀態(tài)下,不同事件轉化的代碼管理,讓其代碼變得更加清晰明了。
在我們的日常生活中電梯是我們非常熟悉的老伙計,電梯有停止、運行、開門、關門等狀態(tài)。如果我們按了某一層按了電梯按鈕(事件),電梯會進入運行狀態(tài),并且運行到目標樓層,然后停止并開門等一系列的動作。類比電梯的運行,Spring的狀態(tài)機也是在有限個狀態(tài)以及這些狀態(tài)之間的轉移和動作等行為,Spring狀態(tài)機的核心概念:
(1)狀態(tài) (State):定義系統可以存在的各個狀態(tài),如電梯的不同階段(停止、運行、開門、關門)。
(2)事件(Event):觸發(fā)狀態(tài)變遷的動作或條件,如用戶點擊“5f”按鈕觸發(fā)電梯的運行事件。
(3)轉換(Transition):定義了在特定狀態(tài)下接收到特定事件時,如何從一個狀態(tài)遷移到另一個狀態(tài)。
(4)動作(Action):可以在狀態(tài)變遷前后執(zhí)行的操作,如狀態(tài)切換時更新數據庫記錄、發(fā)送通知郵件等。
Spring狀態(tài)機的核心在于狀態(tài)變遷和事件驅動,它強調的是系統當前所處的狀態(tài),并且關注于系統如何根據接收到的外部事件或內部條件進行狀態(tài)轉變。
2、實戰(zhàn)Spring狀態(tài)機
我們以典型的電商場景下的訂單狀態(tài)為例,介紹Spring狀態(tài)機如何實現訂單狀態(tài)的高效管理,訂單的狀態(tài)流轉圖如下所示:
圖片
項目的基礎結構圖如下所示:
圖片
(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)的流轉和初始化
@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));    //列舉訂單所有的預設狀態(tài)
    }
    /**
     * 配置訂單狀態(tài)的流轉和觸發(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);
    }
}通過上面的配置就可以實現訂單狀態(tài)直接的轉換,主要用于說明訂單的狀態(tài)和事件的綁定關系,并且指明了訂單狀態(tài)如何流轉的(原始狀態(tài)是什么,目標的狀態(tài)是什么 ,通過什么事件觸發(fā))。
(5)定義監(jiān)聽
訂單的節(jié)點已經定義,那么每個節(jié)點是如何工作的呢?此時需要配置監(jiā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ā)生了轉換后需要執(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ī)則觸發(fā)狀態(tài)的流轉,并執(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í)行的結果
(a)支付的狀態(tài)流轉
圖片
控制臺的輸出結果:
圖片
(b)發(fā)貨的狀態(tài)流轉
圖片
控制臺的輸出結果:
圖片
(c)收貨狀態(tài)的流轉
圖片
控制臺的輸出結果:
圖片
至此,我們實現使用Spring狀態(tài)機實現了訂單的狀態(tài)流轉和觸發(fā)對應的事件。
總結
(1)如果業(yè)務流程復雜,狀態(tài)多,狀態(tài)切換頻繁,Spring狀態(tài)機就非常的適用,它可以提升代碼的維護性和擴展性。
(2)Spring狀態(tài)機可以直接集成到現有的服務中,適合高并發(fā)低延遲的場景,涉及到單個流程的流轉業(yè)務,可以考慮使用狀態(tài)機。















 
 
 








 
 
 
 