首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >10分钟带你体验Spring的状态机

10分钟带你体验Spring的状态机

原创
作者头像
半月无霜
发布2025-02-09 21:37:21
发布2025-02-09 21:37:21
1.2K0
举报
文章被收录于专栏:半月无霜半月无霜

一、介绍

Spring状态机,称为Spring State Machine。它是一种可以管理状态、事件之间的关系,以及他们之间的转换。这是一个专门为应用程序中的状态管理和状态转换提供支持的框架。

它简化了事物对象在不同状态下,不同事件转化的代码管理,让其代码变得更加清晰明了。

本文将介绍Spring State Machine状态机在SpringBoot框架下的使用,10分钟带你理解并上手使用Spring状态机

二、状态机的核心概念

首先,我们必须要了解Spring状态机的几个核心概念,如下

  • 状态(State):代表着对象的当前状态
  • 事件(Event):对象状态转变是因何而改变的
  • 转换(Transition):定义了事物的状态是通过哪个事件变到了另一个状态

打个比方,最常见的就是我们的订单系统,下面就简单模拟一下正常订单的状态流转

  1. 用户下单(状态为:待支付)
  2. 用户支付成功(状态为:已支付待发货)
  3. 仓库已发货(状态为:已发货待收货)
  4. 用户成功收货(状态为:已收货)

这是一个简单的订单交易后,订单发货收货成功的案例状态分析,非常简单

尽管上面的场景有些简单,但不妨碍我们使用此案例去理解状态机

那么我们就要针对上面的这个场景抽取出,状态机的核心概念是什么

首先是订单的状态,是不是有待支付、已支付待发货、已发货待收货、已收货四个状态

我们说的事件就是为用户下单、用户支付成功、仓库已发货、用户成功收货四个事件

那么转换是什么,就是订单状态从已支付待发货,经过了仓库已发货这个事件之后,订单状态就变成了已发货待收货,这样的转换也有4

三、代码

上面已经分析完毕,现在我们将进行代码的编写

我们先添加对应的maven依赖

代码语言:javascript
复制
<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-starter</artifactId>
    <version>4.0.0</version>
</dependency>

对于状态,由于非常固定,我们最好是使用枚举,如下OrderStatusEnum.java

代码语言:javascript
复制
package com.banmoon.enums;
​
import lombok.AllArgsConstructor;
import lombok.Getter;
​
​
@Getter
@AllArgsConstructor
public enum OrderStatusEnum {
​
    WAIT_PAY(0, "待支付"),
    WAIT_DELIVER(1, "已支付待发货"),
    WAIT_RECEIVE(2, "已发货待收货"),
    RECEIVED(3, "已收货");
​
    private final Integer code;
    private final String msg;
​
}

还要定义一下事件的枚举,其实可以不用定义里面的code、msg的,这只是我个人写枚举的一个习惯

代码语言:javascript
复制
package com.banmoon.enums;
​
import lombok.AllArgsConstructor;
import lombok.Getter;
​
@Getter
@AllArgsConstructor
public enum OrderStatusEventEnum {
​
    ORDER(1, "用户下单"),
    PAY(2, "用户支付成功"),
    DELIVER(3, "仓库已发货"),
    RECEIVE(4, "用户成功收货");
​
    private final Integer code;
    private final String msg;
​
}

接下来我们就能定义转换了,在SpringBoot框架下,我们可以直接使用配置类的方式进行定义

代码语言:javascript
复制
package com.banmoon.config;
​
import com.banmoon.enums.OrderStatusEnum;
import com.banmoon.enums.OrderStatusEventEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.*;
​
import java.util.EnumSet;
​
@Slf4j
@Configuration
public class OrderStateMachineConfig extends StateMachineConfigurerAdapter<OrderStatusEnum, OrderStatusEventEnum> {
​
    @Override
    public void configure(StateMachineStateConfigurer<OrderStatusEnum, OrderStatusEventEnum> states) throws Exception {
        states.withStates()
                .initial(OrderStatusEnum.WAIT_PAY)
                .states(EnumSet.allOf(OrderStatusEnum.class));
    }
​
    @Override
    public void configure(StateMachineTransitionConfigurer<OrderStatusEnum, OrderStatusEventEnum> transitions) throws Exception {
        transitions
                // 待支付 -> 已支付待发货(支付成功)
                .withExternal().source(OrderStatusEnum.WAIT_PAY).target(OrderStatusEnum.WAIT_DELIVER).event(OrderStatusEventEnum.PAY)
                .and()
                // 已支付待发货 -> 已发货待收货(仓库已发货)
                .withExternal().source(OrderStatusEnum.WAIT_DELIVER).target(OrderStatusEnum.WAIT_RECEIVE).event(OrderStatusEventEnum.DELIVER)
                .and()
                // 已发货待收货 -> 已收货(用户收货)
                .withExternal().source(OrderStatusEnum.WAIT_RECEIVE).target(OrderStatusEnum.RECEIVED).event(OrderStatusEventEnum.RECEIVE);
    }
​
}

是不是感觉还缺了什么呢,配置中还缺少一个下单事件,这没有关系,这是我们初始的一个事件,不参于配置

我们写对应的controller接口即可,这边模拟一下,创建订单进行落库

代码语言:javascript
复制
    @PostMapping("/create")
    public ResultData<Void> create() {
        // TODO 模拟订单落库
        return ResultData.success();
    }

重点是在后面的,支付这一个阶段,我们应该如何接入状态机,如下

代码语言:javascript
复制
package com.banmoon.controller;
​
import com.banmoon.business.obj.dto.ResultData;
import com.banmoon.entity.OrderEntity;
import com.banmoon.enums.OrderStatusEnum;
import com.banmoon.enums.OrderStatusEventEnum;
import io.swagger.annotations.Api;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.statemachine.StateMachine;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
​
import javax.annotation.Resource;
​
@Slf4j
@Api(tags = "订单状态机测试")
@Validated
@RestController
@RequestMapping("/order")
@RequiredArgsConstructor
public class OrderController {
​
    @Resource
    private StateMachine<OrderStatusEnum, OrderStatusEventEnum> orderStatusMachine;
​
    @PostMapping("/pay")
    public ResultData<Void> pay() {
        // TODO 模拟订单支付
        return ResultData.success();
    }
​
    @PostMapping("/payNotify")
    public ResultData<Void> payNotify(Integer orderId) {
        // TODO 支付成功后,使用状态机
        orderStatusMachine.start();
        // 模拟通过id查找一个订单对象
        OrderEntity entity = new OrderEntity();
        entity.setId(orderId);
        entity.setStatus(OrderStatusEnum.WAIT_PAY.getCode());
        // 将这个对象封装成为一个message
        Message<OrderStatusEventEnum> message = MessageBuilder
                .withPayload(OrderStatusEventEnum.PAY)
                .setHeader("order", entity)
                .build();
        // 使用状态机发送这个消息
        orderStatusMachine.sendEvent(message);
        return ResultData.success();
    }
​
}

在发送消息之后,是不是类似于MQ消息队列那样,有生产者也有消费者。

我们需要创建事件对应的监听处理

代码语言:javascript
复制
package com.banmoon.listener;
​
import com.banmoon.entity.OrderEntity;
import com.banmoon.enums.OrderStatusEnum;
import com.banmoon.enums.OrderStatusEventEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.Message;
import org.springframework.statemachine.annotation.OnTransition;
import org.springframework.statemachine.annotation.WithStateMachine;
import org.springframework.stereotype.Component;
​
@Slf4j
@Component
@WithStateMachine(name = "orderMachine")
public class OrderMachineListener {
​
    @OnTransition(source = "WAIT_PAY", target = "WAIT_DELIVER")
    public void payTransition(Message<OrderStatusEventEnum> message) {
        // 获取消息头中的order对象
        OrderEntity entity = message.getHeaders().get("order", OrderEntity.class);
        entity.setStatus(OrderStatusEnum.WAIT_DELIVER.getCode());
        // TODO 修改数据库中的事件
    }
​
​
}

可以看到@OnTransition注解,定义了源状态,与目标状态

有个疑问,状态机是如何知道要回调这个方法的呢?毕竟后面还有好几个事件 其实很简单,我们在上面的配置类中已经配置过,源状态到目标状态,经历过什么事件 所以我们在发送事件的时候,只需要确认状态也可以反向确定事件的回调

四、最后

上面的代码还非常不完整,但Spring状态机基本的概念都已经讲清楚了

案例缺少下面这三点

  1. 代码不完整,缺少整理一条成功收货的状态机代码
  2. 订单未落库,需要接入数据库使用
  3. 订单不只有成功收货的流程,应当还包括取消订单,拒收退货这些事件

基于上面三点,我会在后面的文章进行优化。

状态机在我们平常业务中,并没有得到很高的重视,但我认为这是有一定必要的。

但其复杂程度,除了给业务增加一点代码上的理解难度外,也并没有很大的一个功能性,自己写的代码逻辑也能跑,还方便理解。属实没有必要,这也是状态机不这么流行的一个原因吧。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、介绍
  • 二、状态机的核心概念
  • 三、代码
  • 四、最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档