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

1,552 阅读20分钟

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

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

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

Spring Statemachine - Reference Documentation

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

基本概念

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

概念定义解释示例
State (状态)代表状态机中的一个具体状态。在状态机中,状态是系统在某一时刻的条件或情境。1、每个状态可以定义进入 (entry) 和退出 (exit) 时的行为。
2、一个状态可以是终态 (end state),当状态机到达这个状态时,状态机的生命周期就结束了。
在订单处理中UNPAIDWAITING_FOR_RECEIVE
Transition(状态转换)表示状态机从一个状态到另一个状态的转换条件。通常伴随着一个事件 (event) 的发生。1、一个 Transition 通常会绑定一个事件 (Event)
2、Transition 还可以有条件 (Guard) 和动作 (Action) 绑定。
订单处理中UNPAIDWAITING_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 构建一个状态机。这个过程是不断演进的,从一个基本的状态机、到添加 ActionGuard、异常处理以及持久化。

状态及事件定义

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

  • 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("用户支付(状态转换结束)");
    }
}

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

使用 mongodb 持久化机制

spring statemachine 在外部化的持久化策略上提供了 3 种,包括 JPAMongoDB 以及 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 中的 createpayreceive 三个接口;

  • 执行 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 类,添加如下代码

@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 中的数据截图如下:

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;因篇幅问题,具体的源码分析和梳理笔者将单独用一篇文章来阐述。

参考