Spring状态机,称为Spring State Machine。它是一种可以管理状态、事件之间的关系,以及他们之间的转换。这是一个专门为应用程序中的状态管理和状态转换提供支持的框架。
它简化了事物对象在不同状态下,不同事件转化的代码管理,让其代码变得更加清晰明了。
本文将介绍Spring State Machine状态机在SpringBoot框架下的使用,10分钟带你理解并上手使用Spring状态机
首先,我们必须要了解Spring状态机的几个核心概念,如下
打个比方,最常见的就是我们的订单系统,下面就简单模拟一下正常订单的状态流转
这是一个简单的订单交易后,订单发货收货成功的案例状态分析,非常简单
尽管上面的场景有些简单,但不妨碍我们使用此案例去理解状态机
那么我们就要针对上面的这个场景抽取出,状态机的核心概念是什么
首先是订单的状态,是不是有待支付、已支付待发货、已发货待收货、已收货四个状态
我们说的事件就是为用户下单、用户支付成功、仓库已发货、用户成功收货四个事件
那么转换是什么,就是订单状态从已支付待发货,经过了仓库已发货这个事件之后,订单状态就变成了已发货待收货,这样的转换也有4个
上面已经分析完毕,现在我们将进行代码的编写
我们先添加对应的maven依赖
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-starter</artifactId>
<version>4.0.0</version>
</dependency>对于状态,由于非常固定,我们最好是使用枚举,如下OrderStatusEnum.java
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的,这只是我个人写枚举的一个习惯
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框架下,我们可以直接使用配置类的方式进行定义
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接口即可,这边模拟一下,创建订单进行落库
@PostMapping("/create")
public ResultData<Void> create() {
// TODO 模拟订单落库
return ResultData.success();
}重点是在后面的,支付这一个阶段,我们应该如何接入状态机,如下
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消息队列那样,有生产者也有消费者。
我们需要创建事件对应的监听处理
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状态机基本的概念都已经讲清楚了
案例缺少下面这三点
基于上面三点,我会在后面的文章进行优化。
状态机在我们平常业务中,并没有得到很高的重视,但我认为这是有一定必要的。
但其复杂程度,除了给业务增加一点代码上的理解难度外,也并没有很大的一个功能性,自己写的代码逻辑也能跑,还方便理解。属实没有必要,这也是状态机不这么流行的一个原因吧。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。