在开发中,无时无刻离不开状态的一个概念,任何一条数据都有属于它的状态。
比如一个电商平台,一个订单会有很多状态,比如待付款、待发货、待收货、完成订单。而这其中每一个状态的改变都随着一个个事件的发生。比如将商品下单但未付款,那么订单就是待付款状态,当触发支付事件,那么订单就能从待付款状态转变未待发货状态,以此类推随之对应的事件就是发货、收货。
其二,状态的流动是固定了的。也就是说,待付款状态的下一个状态只能是待发货状态,不能直接转化为待收货状态。这种由待付款直接转变未待收货的状态是非法的,是程序不允许的。
对于这样的一种情况,最简单的解决方案无疑就是if-lese,比如编写一个支付接口,首先根据订单ID从数据库中查询出来订单信息,然后判断一下订单状态是不是待付款状态,如果是待付款状态,则可以继续下面的流程,否则抛出异常告知用户是非法操作。
这种使用硬编码的if-else实现的效果固然没啥问题,但是如果中间状态出现了改变,比如待付款状态出现一个待拼单,那么代码改动幅度未免太大,难以维护。
这时候,学过设计模式的同学,很容易就想到了状态模式。
状态模式将状态改变抽象成了三个角色:
使用状态模式,可以将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。并且允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
但是状态模式也存在缺点:
对比两种方案,状态模式是更好的解决方案,而对应到实践,也就是状态机。
有限状态机(Finite-state machine,FSM)
,又称有限状态自动机
,简称状态机
,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
而要实现状态之间的流转,必须具备以下几个要素。
1. 当前状态:状态流转的起始状态,如上图中的新建状态
2. 触发事件:引发状态与状态之间流转的事件,如上图中的创建订单这个动作
3. 响应函数:触发事件到下一个状态之间的规则
4. 目标状态:状态流转的终止状态,如上图中的待付款状态
简单来说,只有满足当订单是新建状态并且触发创建订单事件,才会执行触发函数,使得状态由新建转化为待付款。
这就是一个状态机的基本要素,但是要实现一个状态机并不简单,好在Spring为我们提供了Spring StateMachine
框架。
Spring Statemachine
是应用程序开发人员在Spring应用程序中使用状态机概念的框架
Spring Statemachine
旨在提供以下功能:
官网:spring.io/projects/sp…
源码:github.com/spring-proj…
API:docs.spring.io/spring-stat…
状态机是一种用于控制应用程序状态转换的机制。它包含了一组预定义的状态和状态之间的转换规则。在应用程序运行时,通过不同的事件或计时器触发,状态机能够根据事先定义好的规则自动地改变应用程序的状态。这种设计思想使得开发人员能够更加方便地追踪和调试应用程序的行为,因为状态转换的规则是在启动时确定的,而不需要动态地修改或推断。
首先,引入Spring StateMachine
的依赖。
<dependency>
<groupId>org.springframework.statemachinegroupId>
<artifactId>spring-statemachine-coreartifactId>
<version>2.1.3.RELEASEversion>
dependency>
定义订单状态的枚举与触发订单状态改变的事件枚举
/**
* @description: 订单状态
* @author:lrk
* @date: 2023/9/6
*/
@AllArgsConstructor
@Getter
public enum OrderState {
WAIT_PAYMENT(1, "待支付"),
WAIT_DELIVER(2, "待发货"),
WAIT_RECEIVE(3, "待收货"),
FINISH(4, "已完成");
private Integer value;
private String desc;
}
/**
* @description: 事件枚举类
* @author:lrk
* @date: 2023/9/6
*/
public enum OrderStatusChangeEvent {
/**
* 支付
*/
PAYED,
/**
* 发货
*/
DELIVERY,
/**
* 确认收货
*/
RECEIVED
}
创建一个订单表,这里只是简单演示,所有只有id、用户名称和订单状态
CREATE TABLE `t_order` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '下单用户名称',
`status` tinyint NULL DEFAULT NULL COMMENT '订单状态(1:待支付,2:待发货,3:待收货,4:已完成)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
接着,编写状态机的配置类。
/**
* @description: 状态机配置类
* @author:lrk
* @date: 2023/9/6
*/
@Configuration
@EnableStateMachine(name = "orderStateMachine")
@Slf4j
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter {
/**
* 配置初始状态
*/
@Override
public void configure(StateMachineStateConfigurer states) throws Exception {
states.withStates()
// 指定初始化状态
.initial(OrderState.WAIT_PAYMENT)
// 指定解决状态
.end(OrderState.FINISH)
.states(EnumSet.allOf(OrderState.class));
}
/**
* 配置状态转换事件关系
*
* @param transitions
* @throws Exception
*/
@Override
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
transitions
//支付事件:待支付-》待发货
.withExternal().source(OrderState.WAIT_PAYMENT).target(OrderState.WAIT_DELIVER)
.event(OrderStatusChangeEvent.PAYED)
.and()
//发货事件:待发货-》待收货
.withExternal().source(OrderState.WAIT_DELIVER).target(OrderState.WAIT_RECEIVE)
.event(OrderStatusChangeEvent.DELIVERY)
.and()
//收货事件:待收货-》已完成
.withExternal().source(OrderState.WAIT_RECEIVE).target(OrderState.FINISH).event(OrderStatusChangeEvent.RECEIVED);
}
}
接着,编写状态机监听器。
状态机监听器种指定了状态从某个状态到某个状态的时候会触发哪个方法,执行方法的逻辑。
比如订单状态一开始是WAIT_PAYMENT
,需要转化为WAIT_DELIVER
那么就会执行payTransition
方法的逻辑,在这个方法中可以编写相应的业务逻辑。
/**
* @description: 状态机监听器
* @author:lrk
* @date: 2023/9/6
*/
@WithStateMachine(name = "orderStateMachine")
@Slf4j
@Component("orderStateListener")
public class OrderListener {
@Resource
private OrderService orderService;
@OnTransition(source = "WAIT_PAYMENT", target = "WAIT_DELIVER")
public boolean payTransition(Message message) {
Order order = (Order) message.getHeaders().get("order");
order.setStatus(OrderState.WAIT_DELIVER.getValue());
log.info("支付,状态机反馈信息:" + message.getHeaders().toString());
return orderService.updateById(order);
}
@OnTransition(source = "WAIT_DELIVER", target = "WAIT_RECEIVE")
public boolean deliverTransition(Message message) {
Order order = (Order) message.getHeaders().get("order");
order.setStatus(OrderState.WAIT_RECEIVE.getValue());
log.info("发货,状态机反馈信息:" + message.getHeaders().toString());
return orderService.updateById(order);
}
@OnTransition(source = "WAIT_RECEIVE", target = "FINISH")
public boolean receiveTransition(Message message) {
Order order = (Order) message.getHeaders().get("order");
order.setStatus(OrderState.FINISH.getValue());
log.info("收货,状态机反馈信息:" + message.getHeaders().toString());
return orderService.updateById(order);
}
}
接着编写接口
/**
* @description: 订单接口
* @author:lrk
* @date: 2023/9/6
*/
@RestController
@RequestMapping("order")
public class OrderController {
@Resource
private OrderService orderService;
@GetMapping("create")
public BaseResponse create() {
return ResultUtils.success(orderService.create());
}
@GetMapping("pay")
public BaseResponse pay(@RequestParam Integer id) {
return ResultUtils.success(orderService.pay(id));
}
@GetMapping("deliver")
public BaseResponse deliver(@RequestParam Integer id) {
return ResultUtils.success(orderService.deliver(id));
}
@GetMapping("receive")
public BaseResponse receive(@RequestParam Integer id) {
return ResultUtils.success(orderService.receive(id));
}
@GetMapping("getOrders")
public BaseResponse> getOrders() {
return ResultUtils.success(orderService.getOrders());
}
}
/**
* @author lrk
* @description 针对表【t_order】的数据库操作Service实现
* @createDate 2023-09-06 22:42:22
*/
@Service
@Slf4j
public class OrderServiceImpl extends ServiceImpl
implements OrderService {
@Resource
private StateMachine orderStateMachine;
@Resource
private StateMachinePersister persister;
@Override
public Order create() {
Order order = new Order();
order.setName("小明" + UUID.randomUUID());
order.setStatus(OrderState.WAIT_PAYMENT.getValue());
this.save(order);
return order;
}
@Override
public Order pay(int id) {
Order order = this.getById(id);
log.info("支付:order订单信息:{}", order);
if (!sendEvent(OrderStatusChangeEvent.PAYED, order)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "状态转换异常");
}
return this.getById(id);
}
@Override
public Order deliver(int id) {
Order order = this.getById(id);
log.info("发货:order订单信息:{}", order);
if (!sendEvent(OrderStatusChangeEvent.DELIVERY, order)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "状态转换异常");
}
return this.getById(id);
}
@Override
public Order receive(int id) {
Order order = this.getById(id);
log.info("收货:order订单信息:{}", order);
if (!sendEvent(OrderStatusChangeEvent.RECEIVED, order)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "状态转换异常");
}
return this.getById(id);
}
@Override
public List getOrders() {
return this.list();
}
/**
* 发送订单状态转换事件
* synchronized修饰保证这个方法是线程安全的
*
* @param changeEvent
* @param order
* @return
*/
private synchronized boolean sendEvent(OrderStatusChangeEvent changeEvent, Order order) {
boolean result = false;
try {
//启动状态机
orderStateMachine.start();
//尝试恢复状态机状态
persister.restore(orderStateMachine, order);
Message message = MessageBuilder.withPayload(changeEvent).setHeader("order", order).build();
result = orderStateMachine.sendEvent(message);
//持久化状态机状态
persister.persist(orderStateMachine, order);
} catch (Exception e) {
log.error("订单操作失败:{}", e);
} finally {
orderStateMachine.stop();
}
return result;
}
}
其实到这,还需要思考一个问题,在业务层通过状态机发送的只是订单转变事件只是订单状态改变的事件OrderStatusChangeEvent
,那么状态机怎么知道初始状态是什么?因为需要靠初始状态判断是否达到体检可以转变状态。
这就需要配置状态机持久化配置了
/**
* 持久化配置
* 实际使用中,可以配合redis等,进行持久化操作
*
* @return
*/
@Bean
public DefaultStateMachinePersister persister() {
return new DefaultStateMachinePersister<>(new StateMachinePersist
首先状态机会触发read(Order order)
方法,在持久化存储中读取相应的状态机上下文。
这样状态机就能获取到的初始状态了。
而write(StateMachineContext
方法,则是将订单ID对应的上下文放到map集合中去。
根据订单的初始状态和触发事件对应的目标状态,执行相对应的状态机监听器事件。
然后将状态机修改后的订单状态的上下文通过write
方法,写进map中,以便下一次订单状态流转的时候可以用到。
一开始,创建一个订单,订单状态为1,也就是待付款。
接着调用支付接口,触发支付事件,订单状态流转为2,也就是待发货
如果这时候,不调用发货接口,直接调用收货接口,订单状态会不会改变呢?
很明显不会,状态机会识别到状态流转异常,在sendEvent
会返回false
表示失败,接着业务层抛出异常。
继续调用发货接口,订单触发发货事件,订单状态转变为3,也就是待收货状态。
最后,收货,整个订单状态流转过程就完美完成了!
Spring StateMachine
是Spring旗下的一个状态机框架。所以生态非常丰富,与Spring整合度非常高,非常适合结合Spring框架去使用。
但是,Spring StateMachine
定制性难度困难,因为Spring StateMachine
是一个复杂的框架,各方面来说难以定制化。
所以如果是直接使用状态机的组件库,可以考虑使用Spring的状态机。
参考
- Squirrel状态机-从原理探究到最佳实践 - 掘金 (juejin.cn)
- 状态机的介绍和使用 | 京东物流技术团队 - 掘金 (juejin.cn)
- Spring之状态机讲解_spring状态机_爱吃牛肉的大老虎的博客-CSDN博客
- Spring StateMachine 文档 | 中文文档 (gitcode.host)
- 【设计模式】软件设计原则以及23种设计模式总结_起名方面没有灵感的博客-CSDN博客
- 使用Spring StateMachine框架实现状态机 (taodudu.cc)