本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
在之前的一些项目实践中,关于状态变更流转基本都是通过业务逻辑+更新表
的方式来实现的;这种实现方式会在代码中产生较多的条件语句,从可读性上来说还算不错。近期项目中又涉及到一个状态流转的功能需求,因此笔者就期望借此来了解下状态机相关的机制和使用。笔者是基于 spring statemachine 进行的调研;在查找相关资料和构建 demo 的过程中发现,网络上关于 spring statemachine 的一些介绍和使用,除了官方文档在概念上有比较全的概述之外,其他的均不能提供很好的入门指引,特别是在持久化部分。这也是笔者将本篇文章分享出来的原因,期望给各位读者提供一个比较完整的入门和应用案例(此瓜包熟)。
本篇文章中笔者使用的版本是 3.2.1
,springboot
版本是 2.4.12
,jdk
版本是 8
。下面是官方文档的链接地址:
关于 spring statemachine
的介绍本篇不再赘述,为了便于理解和阅读的连贯性,下面会将几个比较重要的概念先抛出来,接着是 step by step
的构建一个完整的 spring statemachine
案例。
基本概念
在 Spring StateMachine
中,下表的概念共同构成了状态机的核心结构;以下是对每个概念的解释。
概念 | 定义 | 解释 | 示例 |
---|---|---|---|
State (状态) | 代表状态机中的一个具体状态。在状态机中,状态是系统在某一时刻的条件或情境。 | 1、每个状态可以定义进入 (entry ) 和退出 (exit ) 时的行为。2、一个状态可以是终态 (end state),当状态机到达这个状态时,状态机的生命周期就结束了。 | 在订单处理中UNPAID 和 WAITING_FOR_RECEIVE |
Transition(状态转换) | 表示状态机从一个状态到另一个状态的转换条件。通常伴随着一个事件 (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 class 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
的作用是检查当前订单号是否合法,本例中如果订单号长度不等于 10 则认为是非法的订单号,则不允许通过。
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 class 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
- OrderIdCheckPassedAction
package 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 class 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("通过检查,等待收货......");
}
}
- OrderIdCheckFailedAction
package 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 class 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 class 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
类实现,另一种是通过注解的方式。
通过继承 StateMachineListenerAdapter
package 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
*/
@Component
public class 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("用户支付(状态转换结束)");
}
}
}
需要在状态机配置类中配置监听器
@Autowired
private OrderStateMachineListener listener;
// 初始化当前状态机配置
@Override
public 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
@WithStateMachine
public class 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
*/
@Configuration
public class StateMachinePersistentConfig {
@Configuration
@Profile("mongo")
public static class 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 class 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 类
@Data
public class Order {
// 订单号
private int id;
// 订单状态
private States states;
public Order(int orderId) {
this.id = orderId;
}
public Order() {
}
@Override
public String toString() {
return "订单号:" + id + ", 订单状态:" + states;
}
}
- OrderStateService 类
package 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
*/
@Service
public class 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 class 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 启动类
@SpringBootApplication
public class 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 中的数据截图如下:
- 执行 pay,日志输出如下
--- 发送PAY事件 ---
用户支付完毕,状态机反馈信息:{order=订单号:1724317856, 订单状态:WAITING_FOR_RECEIVE, id=02bb9d45-901f-be53-b6d0-29a3a8b5e667, timestamp=1724317963599}
检查订单:通过
通过检查,等待收货......
事件是否发送成功:true,当前状态:WAITING_FOR_RECEIVE
此时 mongodb 中的数据截图如下:
- 执行 receive,日志输出如下:
--- 发送RECEIVE事件 ---
用户已收货,状态机反馈信息:{order=订单号:1724317856, 订单状态:DONE, id=dba29317-935b-7d3a-cafa-cdb443b5aab7, timestamp=1724318041106}
事件是否发送成功:true,当前状态:DONE
此时 mongodb 中的数据如下
触发订单检查不通过
前面提到的订单长度不能小于 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
类,添加如下代码
@Autowired
private StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister;
@Override
public 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 中的数据截图如下:
这里有个比较明显的是,当前状态机的 id 是 orderStateMachine
,并非是前面看到的 订单号 id。所以就解释了为什么前后两个订单会产生影响了。细心的读者可能会发现,在配置类中,笔者指定了 machineId
config.withConfiguration().machineId("orderStateMachine").autoStartup(true);
那去掉之后会怎么样呢?会抛出 NPE
这个异常的原因是状态机 ID 是 null。这其实是个悖论,如果指定 machineId
,那么持久化会根据 machineId
作为查询 key ,导致多个订单状态机共享一个持久化状态机,从而相互影响;如果不指定 machineId
则会抛出空指针异常。笔者目前还有没找到比较合适的解决思路,如果有读者有不同的想法,敬请不吝赐教。
关于 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;因篇幅问题,具体的源码分析和梳理笔者将单独用一篇文章来阐述。