聊一聊SpringStateMachine的基本概念和实践

以云看科技 2024-09-19 02:57:50

在之前的一些项目实践中,关于状态变更流转基本都是通过业务逻辑+更新表的方式来实现的;这种实现方式会在代码中产生较多的条件语句,从可读性上来说还算不错。近期项目中又涉及到一个状态流转的功能需求,因此笔者就期望借此来了解下状态机相关的机制和使用。笔者是基于 spring statem)achine 进行的调研;在查找相关资料和构建 demo 的过程中发现,网络上关于 spring statemachine 的一些介绍和使用,除了官方文档在概念上有比较全的概述之外,其他的均不能提供很好的入门指引,特别是在持久化部分。这也是笔者将本篇文章分享出来的原因,期望给各位读者提供一个比较完整的入门和应用案例(此瓜包熟)。

本篇文章中笔者使用的版本是 3.2.1,springboot 版本是 2.4.12,jdk 版本是 8。下面是官方文档的链接地址:

Spring Statemachine - Reference Documentation

关于 spring statemachine 的介绍本篇不再赘述,为了便于理解和阅读的连贯性,下面会将几个比较重要的概念先抛出来,接着是 step by step 的构建一个完整的 spring statemachine 案例。

基本概念

在 Spring StateMachine 中,下表的概念共同构成了状态机的核心结构。以下是对每个概念的解释。

概念定义解释示例State (状态)代表状态机中的一个具体状态。在状态机中,状态是系统在某一时刻的条件或情境。1、每个状态可以定义进入 (entry) 和退出 (exit) 时的行为。2、一个状态可以是终态 (end state),当状态机到达这个状态时,状态机的生命周期就结束了。在订单处理中UNPAID和 WAITING_FOR_RECEIVETransition(状态转换)表示状态机从一个状态到另一个状态的转换条件。通常伴随着一个事件 (event) 的发生。1、一个 Transition 通常会绑定一个事件 (Event)2、Transition 还可以有条件 (Guard) 和动作 (Action) 绑定。订单处理中UNPAID到WAITING_FOR_RECEIVE由 PAY 事件触发Action (动作)在状态转换过程中执行的操作。它可以在状态转换时触发,或者在进入或退出状态时触发。1、Action 可以在状态转换之前 (before transition) 或之后 (after transition) 执行2、Action 是业务逻辑的具体表现,比如记录日志、更新数据库、发送通知等。订单状态从 UNPAID 变为 WAITING_FOR_RECEIVE 时,Action 可以发送短信通知给用户。Guard (守卫条件)一个布尔表达式,用于判断状态转换是否允许执行。它决定了在事件触发时,是否允许从一个状态转换到另一个状态。1、Guard 返回 true,则状态转换可以执行。2、Guard 返回 false,则状态转换不可以执行。一个 Guard 可以检查订单是否已经完成支付,只有当订单支付完成时,才允许状态从 UNPAID 转换到 WAITING_FOR_RECEIVE 。Region (区域)状态机中的一个子状态机,可以看作是状态机内部的一个独立区域,允许并行状态和多个子状态的存在。1、Region 允许状态机在不同的区域中同时处于不同的状态。2、支持复杂的状态模型,比如并行状态或层次化状态。如果一个订单在处理的过程中可以同时进入“支付”流程和“物流”流程,这两个流程可以定义为两个并行的 Region。StateMachine(状态机)StateMachine 是整个状态机的核心组件,管理状态、事件和状态转换。它封装了所有状态、转换、动作、守卫条件等的定义和执行逻辑。1、StateMachine 控制状态的变化和事件的处理。2、可以监听状态变化和转换,提供钩子来执行特定的业务逻辑。-

案例构建

本小节就是 step by step 构建一个状态机。这个过程是不断演进的,从一个基本的状态机、到添加 Action、Guard、异常处理以及持久化。

状态及事件定义

状态机中基本的元素是状态和事件,整个业务逻辑的组织变更流转基本是围绕状态和事件展开。

• States 状态枚举类/** * @Classname State * @Description 状态枚举 * @Date 2024/8/16 13:53 * @Created by glmapper */public enum States { // 未支付 UNPAID, // 待审核 WAITING_FOR_CHECK, // 待收货 WAITING_FOR_RECEIVE, // 结束 DONE;}• Events 事件枚举类/** * @Classname Event * @Description 事件枚举 * @Date 2024/8/16 13:53 * @Created by glmapper */public enum Events { PAY, // 支付 RECEIVE // 收货}状态机定义

用于开启和配置状态机状态以及状态转换条件、动作以及守卫等

package org.glmapper.techssm.configs;// 考虑到很多网上的案例中没有提供准确的 import,笔者这里将 import 也放出来// 除 org.glmapper.techssm 开头的是项目自己的之外,其他的均为三方依赖引入import org.glmapper.techssm.actions.ErrorHandlerAction;import org.glmapper.techssm.actions.OrderIdCheckFailedAction;import org.glmapper.techssm.actions.OrderIdCheckPassedAction;import org.glmapper.techssm.enums.Events;import org.glmapper.techssm.enums.States;import org.glmapper.techssm.guards.OrderIdCheckGuard;import org.springframework.context.annotation.Configuration;import org.springframework.statemachine.config.EnableStateMachine;import org.springframework.statemachine.config.StateMachineConfigurerAdapter;import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;import java.util.EnumSet;/** * @Classname StateMachineConfig * @Description 状态机配置类 * @Date 2024/8/16 13:54 * @Created by glmapper */@Configuration@EnableStateMachine //该注解用来启用 Spring StateMachine 状态机功能public StateMachineConfig extends StateMachineConfigurerAdapter<States, Events> { /** * 初始化当前状态机有哪些状态 * * @param states the {@link StateMachineStateConfigurer} * @throws Exception */ @Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states.withStates().initial(States.UNPAID) // 指定初始状态为未支付 .choice(States.WAITING_FOR_CHECK) // 指定状态为待审核,这里是个选择状态 .states(EnumSet.allOf(States.class)); // 指定 States 中的所有状态作为该状态机的状态定义 } /** * 初始化当前状态机有哪些状态迁移动作, 有来源状态为 source,目标状态为 target,触发事件为 event * <p> * 1、UNPAID -> WAITING_FOR_CHECK 事件 PAY * 2、WAITING_FOR_CHECK * -> WAITING_FOR_RECEIVE 检查通过 * -> UNPAID 检查未通过 * 3、WAITING_FOR_RECEIVE -> DONE 事件 RECEIVE * * @param transitions the {@link StateMachineTransitionConfigurer} * @throws Exception */ @Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions.withExternal() .source(States.UNPAID) .target(States.WAITING_FOR_CHECK) .event(Events.PAY) .and() .withChoice() .source(States.WAITING_FOR_CHECK) .first(States.WAITING_FOR_RECEIVE, new OrderIdCheckGuard(), new OrderIdCheckPassedAction(), new ErrorHandlerAction()) // 如判断为true ->待收货状态 .last(States.UNPAID, new OrderIdCheckFailedAction()) .and() .withExternal() .source(States.WAITING_FOR_RECEIVE) .target(States.DONE) .event(Events.RECEIVE); // 收货事件将触发:待收货状态->结束状态 } }

StateMachineConfig 这个类主要用于定义状态机的核心结构,包括状态(states)、事件(events)、状态之间的转换规则(transitions),以及可能的状态迁移动作和决策逻辑。在 Spring State Machine 中,创建状态机配置类通常是通过继承StateMachineConfigurerAdapter 类来实现的。这个适配器类提供了几个模板方法,允许开发者重写它们来配置状态机的各种组成部分:

• 配置状态(configureStates(StateMachineStateConfigurer)): 在这个方法中,开发者定义状态机中所有的状态,包括初始状态(initial state)和结束状态(final/terminal states)。例如,定义状态 A、B、C,并指定状态A作为初始状态。• 配置转换(configureTransitions(StateMachineTransitionConfigurer)): 在这里,开发者描述状态之间的转换规则,也就是当某个事件(event)发生时,状态机应如何从一个状态转移到另一个状态。例如,当事件X发生时,状态机从状态 A 转移到状态 B。• 配置初始状态(configureInitialState(ConfigurableStateMachineInitializer)): 如果需要显式指定状态机启动时的初始状态,可以在该方法中设置。OrderCheckGuard 定义

OrderIdCheckGuard 的作用是检查当前订单号是否合法,本例中如果订单号小于 100 则认为是非法的订单号,则不允许通过。

import org.glmapper.techssm.enums.Events;import org.glmapper.techssm.enums.States;import org.glmapper.techssm.models.Order;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.statemachine.StateContext;import org.springframework.statemachine.guard.Guard;/** * @Classname OrderIdCheckGuard * @Description 要求订单从“待支付”变为“待收货”状态需要满足某个条件(这里为方便演示,只有订单 id 不小于 100 的才满足条件) * @Date 2024/8/16 14:27 * @Created by glmapper */// 订单检查守卫public OrderIdCheckGuard implements Guard<States, Events> { private static final Logger LOGGER = LoggerFactory.getLogger("SM"); // 检查方法 @Override public boolean evaluate(StateContext<States, Events> context) { // 获取消息中的订单对象 Order order = (Order) context.getMessage().getHeaders().get("order"); // 订单号长度不等于 10 位,则订单号非法 if (String.valueOf(order.getId()).length() != 10) { LOGGER.info("检查订单:不通过,不合法的订单号:" + order.getId()); return false; } else { LOGGER.info("检查订单:通过"); return true; } }}Action 定义

这里的 action 包括针对检查成功和检查失败两个分支逻辑的处理,OrderIdCheckPassedAction 和 OrderIdCheckFailedAction

• OrderIdCheckPassedActionpackage org.glmapper.techssm.actions;import org.glmapper.techssm.enums.Events;import org.glmapper.techssm.enums.States;import org.glmapper.techssm.models.Order;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.statemachine.StateContext;import org.springframework.statemachine.action.Action;/** * @Classname OrderCheckPassedAction * @Description 订单检查通过 Action * @Date 2024/8/16 15:48 * @Created by glmapper */public OrderIdCheckPassedAction implements Action<States, Events> { private static final Logger LOGGER = LoggerFactory.getLogger("SM"); /** * 执行方法 * * @param context 状态上下文 */ @Override public void execute(StateContext<States, Events> context) { // 获取消息中的订单对象 Order order = (Order) context.getMessage().getHeaders().get("order"); // 设置新状态 order.setStates(States.WAITING_FOR_RECEIVE); LOGGER.info("通过检查,等待收货......"); }}• OrderIdCheckFailedActionpackage org.glmapper.techssm.actions;import org.glmapper.techssm.enums.Events;import org.glmapper.techssm.enums.States;import org.glmapper.techssm.models.Order;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.statemachine.StateContext;import org.springframework.statemachine.action.Action;/** * @Classname OrderCheckFailedAction * @Description 订单检查未通过 Action * @Date 2024/8/16 15:49 * @Created by glmapper */public OrderIdCheckFailedAction implements Action<States, Events> { private static final Logger LOGGER = LoggerFactory.getLogger("SM"); /** * 执行方法 * * @param context 状态上下文 */ @Override public void execute(StateContext<States, Events> context) { // 获取消息中的订单对象 Order order = (Order) context.getMessage().getHeaders().get("order"); // 设置新状态 order.setStates(States.UNPAID); LOGGER.info("检查未通过,状态不流转......"); }}• action 异常处理import org.glmapper.techssm.enums.Events;import org.glmapper.techssm.enums.States;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.statemachine.StateContext;import org.springframework.statemachine.action.Action;/** * @Classname ErrorHandlerAction * @Description 如果 action 执行报错了,会执行此类的逻辑 * @Date 2024/8/16 14:35 * @Created by glmapper */public ErrorHandlerAction implements Action<States, Events> { private static final Logger LOGGER = LoggerFactory.getLogger("SM"); @Override public void execute(StateContext<States, Events> context) { RuntimeException exception = (RuntimeException) context.getException(); LOGGER.error("捕获到异常:" + exception); // 将发生的异常信息记录在StateMachineContext中,在外部可以根据这个这个值是否存在来判断是否有异常发生。 context.getStateMachine().getExtendedState().getVariables().put(RuntimeException.class, exception); }}配置一个状态变换监听器

状态机监听器的实现方式有两种,一种是通过继承 StateMachineListenerAdapter 类实现,另一种是通过注解的方式。

通过继承 StateMachineListenerAdapterpackage org.glmapper.techssm.listener;import org.glmapper.techssm.enums.Events;import org.glmapper.techssm.enums.States;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.statemachine.listener.StateMachineListenerAdapter;import org.springframework.statemachine.transition.Transition;import org.springframework.stereotype.Component;/** * @Classname OrderStateMachineListener * @Description 基于 StateMachineListenerAdapter 的状态机监听器实现方式 * @Date 2024/8/20 15:33 * @Created by glmapper */@Componentpublic OrderStateMachineListener extends StateMachineListenerAdapter<States, Events> { private static final Logger LOGGER = LoggerFactory.getLogger(OrderStateMachineListener.class); /** * 在状态机进行状态转换时调用 * * @param transition the transition */ @Override public void transition(Transition<States, Events> transition) { // 当前是未支付状态 if (transition.getTarget().getId() == States.UNPAID) { LOGGER.info("订单创建"); } // 从未支付->待收货状态 if (transition.getSource().getId() == States.UNPAID && transition.getTarget() .getId() == States.WAITING_FOR_RECEIVE) { LOGGER.info("用户支付完毕"); } // 从待收货->完成状态 if (transition.getSource().getId() == States.WAITING_FOR_RECEIVE && transition.getTarget() .getId() == States.DONE) { LOGGER.info("用户已收货"); } } /** * 在状态机开始进行状态转换时调用 * * @param transition the transition */ @Override public void transitionStarted(Transition<States, Events> transition) { // 从未支付->待收货状态 if (transition.getSource().getId() == States.UNPAID && transition.getTarget() .getId() == States.WAITING_FOR_RECEIVE) { LOGGER.info("用户支付(状态转换开始)"); } } /** * 在状态机进行状态转换结束时调用 * * @param transition the transition */ @Override public void transitionEnded(Transition<States, Events> transition) { // 从未支付->待收货状态 if (transition.getSource().getId() == States.UNPAID && transition.getTarget() .getId() == States.WAITING_FOR_RECEIVE) { LOGGER.info("用户支付(状态转换结束)"); } }}

需要在状态机配置类中配置监听器

@Autowiredprivate OrderStateMachineListener listener;// 初始化当前状态机配置@Overridepublic void configure(StateMachineConfigurationConfigurer<States, Events> config) throws Exception { // 设置监听器 config.withConfiguration().listener(listener); }使用注解(本例中使用的方式)package org.glmapper.techssm.configs;import org.glmapper.techssm.enums.Events;import org.glmapper.techssm.enums.States;import org.glmapper.techssm.models.Order;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.context.annotation.Configuration;import org.springframework.messaging.Message;import org.springframework.statemachine.annotation.OnTransition;import org.springframework.statemachine.annotation.OnTransitionEnd;import org.springframework.statemachine.annotation.OnTransitionStart;import org.springframework.statemachine.annotation.WithStateMachine;/** * @Classname StateMachineEventConfig * @Description 基于注解的事件监听器实现方式,可以同于替代 OrderStateMachineListener * @Date 2024/8/16 14:16 * @Created by glmapper */@Configuration@WithStateMachinepublic StateMachineEventConfig { private static final Logger LOGGER = LoggerFactory.getLogger(StateMachineEventConfig.class); @OnTransition(target = "UNPAID") public void create() { LOGGER.info("订单创建"); } @OnTransition(source = "UNPAID", target = "WAITING_FOR_CHECK") public void pay(Message<Events> message) { // 获取消息中的订单对象 Order order = (Order) message.getHeaders().get("order"); // 设置新状态 order.setStates(States.WAITING_FOR_RECEIVE); LOGGER.info("用户支付完毕,状态机反馈信息:" + message.getHeaders().toString()); } @OnTransition(source = "WAITING_FOR_RECEIVE", target = "DONE") public void receive(Message<Events> message) { // 获取消息中的订单对象 Order order = (Order) message.getHeaders().get("order"); // 设置新状态 order.setStates(States.DONE); LOGGER.info("用户已收货,状态机反馈信息:" + message.getHeaders().toString()); } // 监听状态从待检查订单到待收货 @OnTransition(source = "WAITING_FOR_CHECK", target = "WAITING_FOR_RECEIVE") public void checkPassed() { System.out.println("检查通过,等待收货"); } // 监听状态从待检查订单到待付款 @OnTransition(source = "WAITING_FOR_CHECK", target = "UNPAID") public void checkFailed() { System.out.println("检查不通过,等待付款"); } @OnTransitionStart(source = "UNPAID", target = "WAITING_FOR_RECEIVE") public void payStart() { LOGGER.info("用户支付(状态转换开始)"); } @OnTransitionEnd(source = "UNPAID", target = "WAITING_FOR_RECEIVE") public void payEnd() { LOGGER.info("用户支付(状态转换结束)"); }}

@WithStateMachine 是 Spring StateMachine 提供的一个注解,用于将某个类与状态机绑定。它的主要作用是在该类中自动注入状态机实例,并允许你在该类中监听和处理状态机的事件、状态变化等。

使用 mongodb 持久化机制

spring statemachine 在外部化的持久化策略上提供了 3 种,包括 JPA、MongoDB 以及 Redis;具体可以参考:Repository Persistence。下面是本案例中使用 MongoDbPersistingStateMachineInterceptor 的实现。关于持久化下面笔者会单独做介绍。

package org.glmapper.techssm.configs.persister;import org.glmapper.techssm.enums.Events;import org.glmapper.techssm.enums.States;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Profile;import org.springframework.statemachine.data.jpa.JpaPersistingStateMachineInterceptor;import org.springframework.statemachine.data.jpa.JpaStateMachineRepository;import org.springframework.statemachine.data.mongodb.MongoDbPersistingStateMachineInterceptor;import org.springframework.statemachine.data.mongodb.MongoDbStateMachineRepository;import org.springframework.statemachine.persist.DefaultStateMachinePersister;import org.springframework.statemachine.persist.StateMachinePersister;import org.springframework.statemachine.persist.StateMachineRuntimePersister;/** * @Classname StateMachinePersistentConfig * @Description 状态机持久化的配置类,自定义进行状态机持久化配置 * @Date 2024/8/16 14:56 * @Created by glmapper */@Configurationpublic StateMachinePersistentConfig { @Configuration @Profile("mongo") public static MongoStateMachinePersistConfig { @Bean public StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister(MongoDbStateMachineRepository mongoDbStateMachineRepository) { return new MongoDbPersistingStateMachineInterceptor<>(mongoDbStateMachineRepository); } @Bean public StateMachinePersister stateMachinePersister(StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister) { return new DefaultStateMachinePersister(stateMachineRuntimePersister); } } @Configuration @Profile("jpa") public static JpaStateMachinePersistConfig { @Bean public StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister(JpaStateMachineRepository jpaStateMachineRepository) { return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository); } @Bean public StateMachinePersister stateMachinePersister(StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister) { return new DefaultStateMachinePersister(stateMachineRuntimePersister); } }}测试

这里面还涉及到几个类,这里笔者全部放出来,包括 Order 类、OrderStateService类、OrderStateController 类和一个启动类。

• Order@Datapublic Order { // 订单号 private int id; // 订单状态 private States states; public Order(int orderId) { this.id = orderId; } public Order() { } @Override public String toString() { return "订单号:" + id + ", 订单状态:" + states; }}• OrderStateServicepackage org.glmapper.techssm.service;import org.glmapper.techssm.enums.Events;import org.glmapper.techssm.enums.States;import org.glmapper.techssm.models.Order;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.messaging.Message;import org.springframework.messaging.support.MessageBuilder;import org.springframework.statemachine.StateMachine;import org.springframework.statemachine.persist.StateMachinePersister;import org.springframework.stereotype.Service;/** * @Classname ModelStateService * @Description ModelStateService * @Date 2024/8/21 10:14 * @Created by glmapper */@Servicepublic OrderStateService { private static final Logger LOGGER = LoggerFactory.getLogger(OrderStateService.class); /** * 状态机 */ @Autowired private StateMachine<States, Events> stateMachine; /** * 状态机持久化器 */ @Autowired private StateMachinePersister<States, Events, String> stateMachinePersister; public String createModel() throws Exception { int orderId = getOrderId(); Order order = new Order(); order.setStates(States.UNPAID); order.setId(orderId); this.stateMachine.start(); this.stateMachinePersister.persist(stateMachine, String.valueOf(order.getId())); return "订单创建成功,订单号:" + orderId; } public boolean pay(int orderId) { return this.sendMessages(new Order(orderId), stateMachine, Events.PAY); } public boolean receive(int orderId) { return this.sendMessages(new Order(orderId), stateMachine, Events.RECEIVE); } private synchronized boolean sendMessages(Order order, StateMachine<States, Events> stateMachine, Events event) { LOGGER.info("--- 发送" + event + "事件 ---"); try { stateMachinePersister.restore(stateMachine, String.valueOf(order.getId())); Message message = MessageBuilder.withPayload(event).setHeader("order", order).build(); // 构建消息 boolean result = stateMachine.sendEvent(message); LOGGER.info("事件是否发送成功:" + result + ",当前状态:" + stateMachine.getState().getId()); stateMachinePersister.persist(stateMachine, String.valueOf(order.getId())); return result; } catch (Exception e) { e.printStackTrace(); } return false; } private int getOrderId() { // some logic here,创建按时间递增的订单号,提供代码如下,不使用 variant return (int) (System.currentTimeMillis() / 1000); }}• OrderStateController/** * @Classname OrderStateController * @Description 模型状态控制器 * @Date 2024/8/21 10:12 * @Created by glmapper */@RestController@RequestMapping("/api/model/state")public OrderStateController { @Autowired private OrderStateService modelStateService; @RequestMapping("create") public String createModel() { return this.modelStateService.createModel(); } @RequestMapping("pay") public boolean pay(@RequestParam("orderId") int orderId) { return this.modelStateService.pay(orderId); } @RequestMapping("receive") public boolean receive(@RequestParam("orderId") int orderId) { return this.modelStateService.receive(orderId); }}• TechSsmApplication@SpringBootApplicationpublic TechSsmApplication implements CommandLineRunner { private static final Logger LOGGER = LoggerFactory.getLogger(TechSsmApplication.class); public static void main(String[] args) { SpringApplication.run(TechSsmApplication.class, args); }}验证正常逻辑

在启动程序之后分别执行OrderStateController 中的 create、pay 和 receive三个接口;

• 执行 create,日志输出如下:订单创建

此时 mongodb 中的数据截图如下:

image-20240822171155774

• 执行 pay,日志输出如下--- 发送PAY事件 ---用户支付完毕,状态机反馈信息:{order=订单号:1724317856, 订单状态:WAITING_FOR_RECEIVE, id=02bb9d45-901f-be53-b6d0-29a3a8b5e667, timestamp=1724317963599}检查订单:通过通过检查,等待收货......事件是否发送成功:true,当前状态:WAITING_FOR_RECEIVE

此时 mongodb 中的数据截图如下:

image-20240822171326244

• 执行 receive,日志输出如下:--- 发送RECEIVE事件 ---用户已收货,状态机反馈信息:{order=订单号:1724317856, 订单状态:DONE, id=dba29317-935b-7d3a-cafa-cdb443b5aab7, timestamp=1724318041106}事件是否发送成功:true,当前状态:DONE

此时 mongodb 中的数据如下

image-20240822171429430

触发订单检查不通过

前面提到的订单长度不能小于 10,这里需要在代码中魔改,假设订单号是 9999。执行结果大致如下:

--- 发送PAY事件 ---用户支付完毕,状态机反馈信息:{order=订单号:65, 订单状态:WAITING_FOR_RECEIVE, id=563c41b5-eaea-6c39-cc83-87106acd0591, timestamp=1724318183133}检查订单:不通过,不合法的订单号:65检查未通过,状态不流转......事件是否发送成功:true,当前状态:UNPAID

可以看到在执行了 PAY 事件时,因为订单号检查不通过,因此状态没有发生变化。至此案例部分就完结了。

源码可以在我的掘金首页给我留言获取

持久化和序列化

先说序列化,官方文档中提到目前仅支持 kryo 进行序列化,笔者最开始在进行持久化的实现时踩坑无数,已经到修改源码构建自定义持久化机制的地步,因此就自然而然的用到了它提供的序列化器,这个在源码中是和 Repository 机制算是绑定的

private final StateMachineSerialisationService<S, E> serialisationService; /** * Instantiates a new repository state machine persist. */ protected RepositoryStateMachinePersist() { this.serialisationService = new KryoStateMachineSerialisationService<S, E>(); }

序列化就先暂放一边。来聊一下持久化。网上关于持久化大多是基于内存和 redis 的实现的,和官网上提供的基于拦截器的持久化方式不同。

直接使用拦截器方式进行持久化

首先是将 OrderStateService 中的 sendMessages 方法中进行持久化的相关逻辑注释掉

private synchronized boolean sendMessages(Order order, StateMachine<States, Events> stateMachine, Events event) { LOGGER.info("--- 发送" + event + "事件 ---"); try { //stateMachinePersister.restore(stateMachine, String.valueOf(order.getId())); Message message = MessageBuilder.withPayload(event).setHeader("order", order).build(); // 构建消息 boolean result = stateMachine.sendEvent(message); LOGGER.info("事件是否发送成功:" + result + ",当前状态:" + stateMachine.getState().getId()); //stateMachinePersister.persist(stateMachine, String.valueOf(order.getId())); return result; } catch (Exception e) { e.printStackTrace(); } return false;}

然后配置状态机配置类中配置持久化,修改 StateMachineConfig 类,添加如下代码

@Autowiredprivate StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister;@Overridepublic void configure(StateMachineConfigurationConfigurer<States, Events> config) throws Exception { config.withPersistence().runtimePersister(stateMachineRuntimePersister); // set machineId or not? config.withConfiguration().machineId("orderStateMachine").autoStartup(true);}

测试步骤如下:

• 1、调用 create 接口创建一个新的订单和状态机• 2、调用 pay 方法并且传入 1 中产生的订单• 3、调用 create 接口创建一个新的订单和状态机• 4、调用 receive 方法并且传入 1 中产生的订单• 5、调用 pay 方法并且传入 3 中产生的订单

输出日志如下(订单创建的日志因未涉及到状态流转,因此不会触发想要的事件监听执行):

--- 发送PAY事件 --- // 这里是步骤 1 订单的 PAY用户支付完毕,状态机反馈信息:{order=订单号:1724379861, 订单状态:WAITING_FOR_RECEIVE, id=2b33c417-366e-03d3-a1d5-9c786d1cd669, timestamp=1724379883764}用户支付完毕,状态机反馈信息:{order=订单号:1724379861, 订单状态:WAITING_FOR_RECEIVE, id=2b33c417-366e-03d3-a1d5-9c786d1cd669, timestamp=1724379883764}检查订单:通过通过检查,等待收货......事件是否发送成功:true,当前状态:WAITING_FOR_RECEIVE--- 发送RECEIVE事件 --- // 这里是步骤 1 订单的 RECEIVE用户已收货,状态机反馈信息:{order=订单号:1724379861, 订单状态:DONE, id=fb9e4c14-2e6d-3cea-7fe0-6b1db20152e0, timestamp=1724379980224}用户已收货,状态机反馈信息:{order=订单号:1724379861, 订单状态:DONE, id=fb9e4c14-2e6d-3cea-7fe0-6b1db20152e0, timestamp=1724379980224}事件是否发送成功:true,当前状态:DONE--- 发送PAY事件 --- // 这里是步骤 3 订单的 PAY事件是否发送成功:false,当前状态:DONE

可以看到,当上述步骤3 中创建的订单执行 PAY 事件时,结果是 FALSE,因为状态已经是 DONE,也就是说前一个订单的结束对当前订单产生了影响,此时 mongodb 中的数据截图如下:

聊一聊 Spring StateMachine 的基本概念和实践

image-20240823103840593

这里有个比较明显的是,当前状态机的 id 是 orderStateMachine ,并非是前面看到的 订单号 id。所以就解释了为什么前后两个订单会产生影响了。细心的读者可能会发现,在配置类中,笔者指定了 machineId

config.withConfiguration().machineId("orderStateMachine").autoStartup(true);

那去掉之后会怎么样呢?会抛出 NPE

image-20240823104154720

这个异常的原因是状态机 ID 是 null。这其实是个悖论,如果指定 machineId,那么持久化会根据 machineId 作为查询 key ,导致多个订单状态机共享一个持久化状态机,从而相互影响;如果不指定 machineId 则会抛出空指针异常。笔者目前还有没找到比较合适的解决思路,如果有读者有不同的想法,敬请不吝赐教。

A question about persistence use

关于 StateMachineFactory 和 StateMachineModelFactory

这两个 Factory 也是笔者在尝试解决上述问题时捎带看的,本质是期望能够通过 StateMachineFactory 来为每个订单创建一个新的状态机实例,从而解决前面提到的共享同一个状态机的问题;但是问题在于 StateMachineFactory 确实会为每个请求创建新的状态机,但是它并不能有效的和持久化机制协同起来工作。下面是具体原因。

StateMachine<States, Events> machine = this.stateMachineFactory.getStateMachine(String.valueOf(orderId));

上面这段代码是通过 stateMachineFactory 来创建 StateMachine 的,按照常规的思路,在 getStateMachine 的方法实现中,理论上是需要支持从外部存储中获取 StateMachine 的,官方文档也确实是这样描述的;但是笔者通过简单的测试之后的理解是,这里的 从外部存储中获取 StateMachine 并非是持久化后的恢复,而是外部储存中提供了原始的 stateMachineModel,使得可以通过 stateMachineModel 来构建一个新的 StateMachine。下面的这段异常堆栈即是使用 StateMachineFactory + RepositoryStateMachineModelFactory 后测试得到的,因为笔者没有在存储库中提供任何 模型和转换的定义。

org.springframework.statemachine.config.model.MalformedConfigurationException: Must have at least one transition at org.springframework.statemachine.config.model.verifier.BaseStructureVerifier.verify(BaseStructureVerifier.java:43) at org.springframework.statemachine.config.model.verifier.CompositeStateMachineModelVerifier.verify(CompositeStateMachineModelVerifier.java:43) at org.springframework.statemachine.config.AbstractStateMachineFactory.getStateMachine(AbstractStateMachineFactory.java:174) at org.springframework.statemachine.config.AbstractStateMachineFactory.getStateMachine(AbstractStateMachineFactory.java:149)原理概述

最后一个小节,笔者还是来剖析一下状态机的基本原理。总的来说是:Spring 状态机的基本原理是通过状态、事件和转换来管理对象的状态流转。状态机定义了对象的可能状态(State)及其之间的转换(Transition)。事件(Event)触发状态间的转换,并可能执行特定动作(Action)。状态机由状态(State)、事件(Event)、动作(Action)、守护(Guard)等组成,配置完成后,状态机根据输入事件变更状态。

关于源码这块,因为 3.x 版本整体代码通过 Reactor 进行了重构,整体的代码可读性和 debug 上相比来说比较不友好,所以推荐有意向的读者可以基于 2.5.x 版本进行阅读分析和 debug;因篇幅问题,具体的源码分析和梳理笔者将单独用一篇文章来阐述。

作者:glmapper

来源-微信公众号:磊叔的技术博客

出处:https://mp.weixin.qq.com/s/-y8t4jRViGDszv1toZpTdQ

0 阅读:0