SpringBoot 实战(四十)集成 Statemachine

目录

    • [一、Statemachine 简介](#一、Statemachine 简介)
      • [1.1 核心概念](#1.1 核心概念)
      • [1.2 主要特性](#1.2 主要特性)
      • [1.3 注解驱动开发方式](#1.3 注解驱动开发方式)
      • [1.4 核心注解详解](#1.4 核心注解详解)
        • [1)@WithStateMachine 注解](#1)@WithStateMachine 注解)
        • [2)@OnTransition 注解](#2)@OnTransition 注解)
        • [3)@OnTransitionStart 注解](#3)@OnTransitionStart 注解)
        • [4)@OnTransitionEnd 注解](#4)@OnTransitionEnd 注解)
      • [1.5 包含的模块](#1.5 包含的模块)
    • 二、知识回顾------状态模式
      • [2.1 什么是状态模式?](#2.1 什么是状态模式?)
      • [2.2 状态模式的优缺点](#2.2 状态模式的优缺点)
      • [2.3 状态模式的实现结构](#2.3 状态模式的实现结构)
    • [三、SpringBoot 集成](#三、SpringBoot 集成)
      • [3.1 Maven 依赖](#3.1 Maven 依赖)
      • [3.2 定义状态和事件枚举](#3.2 定义状态和事件枚举)
      • [3.3 配置状态机](#3.3 配置状态机)
      • [3.4 使用注解实现状态监听器](#3.4 使用注解实现状态监听器)
      • [3.5 业务服务类](#3.5 业务服务类)
      • [3.6 控制器类](#3.6 控制器类)
      • [3.7 测试结果](#3.7 测试结果)
    • 四、升级:状态机持久化
      • [4.1 Maven 依赖](#4.1 Maven 依赖)
      • [4.2 自定义持久化类](#4.2 自定义持久化类)
      • [4.3 编写状态机工具类](#4.3 编写状态机工具类)
      • [4.4 修改业务调用](#4.4 修改业务调用)
      • [4.5 测试结果](#4.5 测试结果)

一、Statemachine 简介

Spring Statemachine 是一个由 Spring 团队提供的 轻量级状态机框架 ,它允许开发者以简便且强大的方式管理复杂的状态流转逻辑。该框架建立在 有限状态机(FSM) 的概念之上,提供了一种简洁且灵活的方式来定义、管理和执行状态机。

1.1 核心概念

  • 状态(State):系统可能处于的不同条件或模式,是状态机的核心组成单元。
  • 事件(Event):触发状态转换的动作或消息,是引起状态机从当前状态迁移到新状态的原因。
  • 转换(Transition):描述了在何种条件下,当接收到特定事件时,系统可以从一个状态转移到另一个状态。
  • 动作(Action):在状态转换时执行的具体操作。

1.2 主要特性

Spring Statemachine 提供了丰富的功能特性:

  • 易于使用的平面(一级)状态机,适用于简单的用例;
  • 分层状态结构,以简化复杂的状态配置;
  • 状态机区域提供更复杂的状态配置;
  • 触发器、转换、守卫和动作的使用;
  • 类型安全的配置适配器;
  • 状态机事件监听器;
  • Spring IoC 集成将 Bean 与 状态机 相关联。

1.3 注解驱动开发方式

为了简化开发,可以使用 Statemachine 的 注解驱动开发方式 ,特别是 @OnTransition@OnTransitionStart@OnTransitionEnd@WithStateMachine 注解的使用,这些注解能够让我们以更加 声明式简介 的方式处理状态转换逻辑。

注解驱动的开发方式具有以下优势:

  • 代码简洁性:将状态转换逻辑直接注解在方法上,减少模板代码;
  • 关注点分离:业务逻辑与状态机配置清晰分离,提高可维护性;
  • 类型安全:编译时检查注解的正确性,减少运行时错误;
  • 可读性强:通过注解直观地表达状态转换的意图。

1.4 核心注解详解

1)@WithStateMachine 注解

@WithStateMachine 注解用于标识一个类是与状态机相关的监听器,它告诉 Spring 这个类中的方法需要接受状态机的事件通知。

  • 使用场景:标记状态机监听器类,是类中的状态转换注解生效。
2)@OnTransition 注解

@OnTrasition 注解用于标记在状态转换发生时执行的方法,它不区分转换的开始和结束。

  • 使用场景:当不关心转换的具体阶段,只需要在转换发生时执行某些逻辑时使用。
3)@OnTransitionStart 注解

@OnTransitionStart 注解用于标记在状态转换 开始时 执行的方法。

  • 使用场景:需要在状态转换刚开始时执行与处理逻辑,如参数验证、资源准备等。
4)@OnTransitionEnd 注解

@OnTransitionEnd 注解用于标记在状态转换 结束时 执行的方法。

  • 使用场景:需要在状态转换完成后执行清理逻辑、记录日志、发送通知等。

1.5 包含的模块

Spring Statemachine 包含的模块如下:

模块 描述
spring-statemachine-core Spring Statemachine的核心系统。
spring-statemachine-recipes-common 不需要核心框架之外的依赖项的常见配方。
spring-statemachine-kryo KryoSpring Statemachine的序列化程序。
spring-statemachine-data-common Spring Data的通用支持模块。
spring-statemachine-data-jpa 支持Spring Data JPA模块。
spring-statemachine-data-redis 支持Spring Data Redis模块。
spring-statemachine-data-mongodb 支持Spring Data MongoDB模块。
spring-statemachine-zookeeper 分布式状态机的Zooeman集成。
spring-statemachine-test 状态机测试支持模块。
spring-statemachine-cluster Spring Cloud Cluster的支持模块。请注意,Spring Cloud Cluster已被Spring Integration取代。
spring-statemachine-uml 使用Eclipse Papyrus进行UI UML建模的支持模块。
spring-statemachine-autoconfigure Spring Boot的支持模块。
spring-statemachine-bom 物料清单pom。
spring-statemachine-starter 弹簧启动启动器。

二、知识回顾------状态模式

2.1 什么是状态模式?

状态模式(State Pattern) 是一种 行为型 设计模式,对有状态的对象,把复杂的 "判断逻辑" 提取到不同的状态对象中,允许状态对象在其内部状态发生改变时,改变其行为。

2.2 状态模式的优缺点

状态模式的优点:

  1. 结构清晰 :状态模式将与特定状态相关的行为局部化道一个状态中,并且将不同状态的行为分割开来,满足 "但一职责原则"。
  2. 将状态转换显示化:减少对象间的相互依赖,将不同的状态引入独立的对象中会是的状态转换变得更加明确,且减少对相见的相互依赖。
  3. 状态类职责明确:有利于程序的扩展。通过定义新的子类很容易地增加新的状态和转换。

状态模式的缺点:

  1. 状态模式的使用必然会增加系统的类与对象的个数。
  2. 状态模式的结构与实现都较为复杂,如果使用不当会导致程序结构和代码的混乱。
  3. 状态模式对开闭原则的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源码,否则无法切换到新增状态,而且修改某个状态类的行为也需要修改对应类的源码。

2.3 状态模式的实现结构

状态模式把受环境改变的对象行为包装在不同的状态对象里,其意图是让一个对象在其内部状态改变的时候,其行为也随之改变。现在我们来分析其基本结构和实现方法。

状态模式主要包含三个角色:

  • Context(环境类):定义客户端感兴趣的接口,维护一个 State 子类的实例,这个示例定义当前状态。
  • State(抽象状态类):定义一个接口,用以封装 Context 的特定状态相关的行为。
  • ConcreteState(具体状态类):每一个子类实现一个与 Context 的一个状态相关的行为。

三、SpringBoot 集成

项目结构如下:

3.1 Maven 依赖

对于 SpringBoot 2.x 项目,可以使用 2.x 版本的 StateMachine 依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-starter</artifactId>
    <version>2.2.3.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-kryo</artifactId>
    <version>2.2.3.RELEASE</version>
</dependency>

3.2 定义状态和事件枚举

首先,我们需要定义状态机和事件的所有可能值。以订单系统为例:

OrderStatesEnum.javaOrderEventsEnum.java

java 复制代码
public enum OrderStatesEnum {
    UNPAID,                 // 待支付
    WAITING_FOR_RECEIVE,    // 待收货
    DONE,                   // 完成
    CANCELLED               // 取消
}

public enum OrderEventsEnum {
    PAY,                    // 支付
    RECEIVE,                // 收货
    CANCEL                  // 取消
}

3.3 配置状态机

接下来,我们需要配置状态机,定义状态转换规则:

StateMachineConfig.java

java 复制代码
import com.demo.enums.OrderEventsEnum;
import com.demo.enums.OrderStatesEnum;
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;

@Configuration
@EnableStateMachine(name = "orderStateMachine")
public class StateMachineConfig extends StateMachineConfigurerAdapter<OrderStatesEnum, OrderEventsEnum> {

    @Override
    public void configure(StateMachineStateConfigurer<OrderStatesEnum, OrderEventsEnum> states) throws Exception {
        states
                .withStates()
                .initial(OrderStatesEnum.UNPAID)
                .states(EnumSet.allOf(OrderStatesEnum.class));
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<OrderStatesEnum, OrderEventsEnum> transitions) throws Exception {
        transitions
                .withExternal()
                .source(OrderStatesEnum.UNPAID).target(OrderStatesEnum.WAITING_FOR_RECEIVE)
                .event(OrderEventsEnum.PAY)
                .and()
                .withExternal()
                .source(OrderStatesEnum.WAITING_FOR_RECEIVE).target(OrderStatesEnum.DONE)
                .event(OrderEventsEnum.RECEIVE)
                .and()
                .withExternal()
                .source(OrderStatesEnum.UNPAID).target(OrderStatesEnum.CANCELLED)
                .event(OrderEventsEnum.CANCEL);
    }
}

3.4 使用注解实现状态监听器

这段代码展示如何使用注解来监听状态转换:

OrderStateListener.java

java 复制代码
import com.demo.enums.OrderEventsEnum;
import com.demo.enums.OrderStatesEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.annotation.OnTransition;
import org.springframework.statemachine.annotation.OnTransitionEnd;
import org.springframework.statemachine.annotation.OnTransitionStart;
import org.springframework.statemachine.annotation.WithStateMachine;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@WithStateMachine(name = "orderStateMachine")
public class OrderStateListener {

    /**
     * 支付转换开始时的处理
     */
    @OnTransitionStart(source = "UNPAID", target = "WAITING_FOR_RECEIVE")
    public void onPayStart(StateContext<OrderStatesEnum, OrderEventsEnum> context) {
        log.info("【支付转换开始】开始处理支付逻辑");

        // 获取转换相关的数据
        Object paymentData = context.getMessageHeader("paymentData");
        if (paymentData != null) {
            log.info("支付数据:{}", paymentData);
        }

        // 执行支付前的验证逻辑
        log.info("验证支付参数...");
        log.info("检查库存...");
        log.info("预扣库存...");
    }

    /**
     * 支付转换结束时的处理
     */
    @OnTransitionEnd(source = "UNPAID", target = "WAITING_FOR_RECEIVE")
    public void onPayEnd(StateContext<OrderStatesEnum, OrderEventsEnum> context) {
        log.info("【支付转换结束】支付处理完成");

        // 执行支付后的清理逻辑
        log.info("更新库存...");
        log.info("生成支付凭证...");
        log.info("发送支付成功通知...");

        // 记录转换耗时
        Long startTime = (Long) context.getMessageHeader("startTime");
        if (startTime != null) {
            long duration = System.currentTimeMillis() - startTime;
            log.info("支付处理耗时:{}ms", duration);
        }
    }

    /**
     * 收货转换开始时的处理
     */
    @OnTransitionStart(source = "WAITING_FOR_RECEIVE", target = "DONE")
    public void onReceiveStart() {
        log.info("【收货转换开始】开始确认收货");
        log.info("验证收货权限...");
        log.info("检查物流信息...");
    }

    /**
     * 收货转换结束时的处理
     */
    @OnTransitionEnd(source = "WAITING_FOR_RECEIVE", target = "DONE")
    public void onReceiveEnd() {
        log.info("【收货转换结束】收货确认完成");
        log.info("更新订单完成时间...");
        log.info("计算商家评分...");
        log.info("发送订单完成通知...");
    }

    /**
     * 取消订单转换开始时的处理
     */
    @OnTransitionStart(source = "UNPAID", target = "CANCELLED")
    public void onCancelStart(StateContext<OrderStatesEnum, OrderEventsEnum> context) {
        log.info("【取消转换开始】开始取消订单");

        String cancelReason = context.getMessageHeaders().get("cancelReason", String.class);
        log.info("取消原因: {}", cancelReason);
        log.info("验证取消权限...");
    }

    /**
     * 取消订单转换结束时的处理
     */
    @OnTransitionEnd(source = "UNPAID", target = "CANCELLED")
    public void onCancelEnd() {
        log.info("【取消转换结束】订单取消完成");
        log.info("释放库存...");
        log.info("发送取消通知...");
        log.info("记录取消日志...");
    }

    /**
     * 通用的状态转换处理(不区分开始和结束)
     */
    @OnTransition
    public void onAnyTransition() {
        log.info("【通用转换】状态发生变化");
    }

    /**
     * 从任意状态到指定状态的转换结束处理
     */
    @OnTransitionEnd(target = "DONE")
    public void onTransitionToDone() {
        log.info("【到达完成状态】订单流程结束");
        log.info("执行订单完成后的统计任务...");
        log.info("更新用户积分...");
    }
}

3.5 业务服务类

在业务服务类中使用状态机:

OrderService.java

java 复制代码
import com.demo.enums.OrderStatesEnum;

import java.util.Map;

public interface OrderService {

    /**
     * 处理支付
     */
    void payOrder(String orderId, Map<String, Object> paymentData);

    /**
     * 确认收货
     */
    void confirmReceive(String orderId);

    /**
     * 取消订单
     */
    void cancelOrder(String orderId, String reason);

    /**
     * 获取当前状态
     */
    OrderStatesEnum getCurrentState();
}

OrderServiceImpl.java

java 复制代码
import com.demo.enums.OrderEventsEnum;
import com.demo.enums.OrderStatesEnum;
import com.demo.service.OrderService;
import lombok.extern.slf4j.Slf4j;
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.state.State;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;


@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private StateMachine<OrderStatesEnum, OrderEventsEnum> stateMachine;

    @Override
    public void payOrder(String orderId, Map<String, Object> paymentData) {
        log.info("开始处理订单任务状态事件,orderId: {},paymentData:{}", orderId, paymentData);
        
        // 设置消息头,传递业务数据
        Map<String, Object> headers = new HashMap<>();
        headers.put("orderId", orderId);
        headers.put("paymentData", paymentData);
        headers.put("startTime", System.currentTimeMillis());

        sendStateMachineEvent(OrderStatesEnum.UNPAID, OrderEventsEnum.PAY, headers);
    }

    @Override
    public void confirmReceive(String orderId) {
        log.info("开始处理确认收货状态事件,orderId: {}", orderId);
        
        Map<String, Object> headers = new HashMap<>();
        headers.put("orderId", orderId);

        sendStateMachineEvent(OrderStatesEnum.WAITING_FOR_RECEIVE, OrderEventsEnum.RECEIVE, headers);
    }

    @Override
    public void cancelOrder(String orderId, String reason) {
        log.info("开始处理取消订单状态事件,orderId: {},reason:{}", orderId, reason);
        
        Map<String, Object> headers = new HashMap<>();
        headers.put("orderId", orderId);
        headers.put("cancelReason", reason);

        sendStateMachineEvent(OrderStatesEnum.UNPAID, OrderEventsEnum.CANCEL, headers);
    }

    /**
     * 发送状态机事件的通用方法
     * @param currentState 当前状态
     * @param event 要发送的事件
     * @param headers 消息头数据
     */
    private void sendStateMachineEvent(OrderStatesEnum currentState, OrderEventsEnum event, Map<String, Object> headers) {
        // 启动状态机
        stateMachine.start();

        // 根据当前任务状态设置状态机状态
        log.info("当前任务状态: {}", currentState);

        // 将状态机的状态设置为业务对象的实际状态
        stateMachine.getStateMachineAccessor().doWithAllRegions(accessor -> {
            accessor.resetStateMachine(new org.springframework.statemachine.support.DefaultStateMachineContext<>(
                    currentState, null, null, null));
        });

        // 构建并发送消息
        Message<OrderEventsEnum> message = MessageBuilder
                .withPayload(event)
                .copyHeaders(headers)
                .build();

        stateMachine.sendEvent(message);
    }

    @Override
    public OrderStatesEnum getCurrentState() {
        State<OrderStatesEnum, OrderEventsEnum> state = stateMachine.getState();
        return state == null ? null : state.getId();
    }
}

3.6 控制器类

提供 REST API 接口:

OrderController.java

java 复制代码
import com.demo.common.Result;
import com.demo.enums.OrderStatesEnum;
import com.demo.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
 * <p> @Title DemoController
 * <p> @Description 测试Controller
 *
 * @author ACGkaka
 * @date 2023/4/24 18:02
 */
@Slf4j
@RestController
@RequestMapping("/orders")
public class OrderController {

    @Autowired
    private OrderService orderService;

    @PostMapping("/{orderId}/pay")
    public Result<Object> payOrder(@PathVariable String orderId,
                                   @RequestBody Map<String, Object> paymentData) {
        try {
            orderService.payOrder(orderId, paymentData);
            return Result.succeed("支付处理中");
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return Result.failed("支付失败:" + e.getMessage());
        }
    }

    @PostMapping("/{orderId}/receive")
    public Result<Object> receiveOrder(@PathVariable String orderId) {
        try {
            orderService.confirmReceive(orderId);
            return Result.succeed("收货确认处理中");
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return Result.failed("收货确认失败:" + e.getMessage());
        }
    }

    @PostMapping("/{orderId}/cancel")
    public Result<Object> cancelOrder(@PathVariable String orderId,
                                      @RequestParam String reason) {
        try {
            orderService.cancelOrder(orderId, reason);
            return Result.succeed("取消订单处理中");
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return Result.failed("取消订单失败:" + e.getMessage());
        }
    }

    @GetMapping("/{orderId}/status")
    public Result<OrderStatesEnum> getOrderStatus() {
        try {
            Result<OrderStatesEnum> result = new Result<>();
            OrderStatesEnum currentState = orderService.getCurrentState();
            return result.setData(currentState);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return Result.failed("获取订单状态失败:" + e.getMessage());
        }
    }
}

3.7 测试结果

测试1:订单-支付订单接口

  • 日志打印:

测试2:订单-收货确认接口

  • 日志打印:

测试3:订单-取消订单接口

  • 日志打印:

测试4:查询状态接口

问题点:所有订单共享一个状态

虽然已经完成了状态机的基础操作,但是这里会发现一个问题:整个状态机只有一个状态。也就是说不管是哪个订单的状态都是一样的,那么有一个订单的状态为 DONE 的话,其余所有订单都走不了流程了,只能重启程序才能还原。

这肯定不行,所以就需要 将状态进行持久化,根据订单编号分别保存各自的状态


四、升级:状态机持久化

使用 spring-statemachine 状态机持久化时,可以通过内存、spring-statemachine-redis 或 spring-statemachine-data-jpa 现有方式进行持久化处理。

因项目状态变化操作记录频繁,数据量大,使用 内存 或 spring-statemachine-redis 模式不可取,而项目使用的是 MyBatis,使用 spring-statemachine-data-jpa 也不合适,需要自定义实现。

项目结构如下:

4.1 Maven 依赖

xml 复制代码
<!-- Statemachine -->
<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-starter</artifactId>
    <version>2.2.3.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-kryo</artifactId>
    <version>2.2.3.RELEASE</version>
</dependency>

4.2 自定义持久化类

CustomStateMachinePersist.java

java 复制代码
import com.demo.common.redis.util.RedisUtil;
import com.demo.domain.OrderInfo;
import com.demo.enums.OrderEventsEnum;
import com.demo.enums.OrderStatesEnum;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.MessageHeaders;
import org.springframework.statemachine.StateMachineContext;
import org.springframework.statemachine.StateMachinePersist;
import org.springframework.statemachine.kryo.MessageHeadersSerializer;
import org.springframework.statemachine.kryo.StateMachineContextSerializer;
import org.springframework.statemachine.kryo.UUIDSerializer;
import org.springframework.statemachine.persist.DefaultStateMachinePersister;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.UUID;

/**
 * 自定义状态机持久化
 */
@Slf4j
@Configuration
public class CustomStateMachinePersist {

    private static final String REDIS_KEY_PREFIX = "ORDER_STATE_V1_";

    @Autowired
    private RedisUtil redisUtil;

    private static final ThreadLocal<Kryo> KRYO_THREAD_LOCAL = ThreadLocal.withInitial(() -> {
        Kryo kryo = new Kryo();
        kryo.addDefaultSerializer(StateMachineContext.class, new StateMachineContextSerializer<>());
        kryo.addDefaultSerializer(MessageHeaders.class, new MessageHeadersSerializer());
        kryo.addDefaultSerializer(UUID.class, new UUIDSerializer());
        // 设置引用追踪策略
        kryo.setReferences(true);
        return kryo;
    });

    private <S, E> byte[] serialize(StateMachineContext<S, E> context) {
        Kryo kryo = KRYO_THREAD_LOCAL.get();
        // 重置引用状态,避免状态污染
        kryo.reset();

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        Output output = new Output(out);
        try {
            kryo.writeObject(output, context);
            output.flush();
            return out.toByteArray();
        } catch (Exception e) {
            log.error("序列化状态机上下文失败", e);
            throw new RuntimeException("序列化失败", e);
        } finally {
            output.close();
        }
    }

    private <S, E> StateMachineContext<S, E> deserialize(byte[] data) {
        if (data == null || data.length == 0) {
            log.info("反序列化数据为空");
            return null;
        }

        Kryo kryo = KRYO_THREAD_LOCAL.get();
        // 重置引用状态,避免状态污染
        kryo.reset();

        ByteArrayInputStream in = new ByteArrayInputStream(data);
        Input input = new Input(in);
        try {
            return kryo.readObject(input, StateMachineContext.class);
        } catch (IndexOutOfBoundsException e) {
            log.error("反序列化失败,可能是数据损坏或版本不兼容. 数据长度: {}, 错误: {}",
                    data.length, e.getMessage());
            throw new RuntimeException("反序列化失败", e);
        } catch (Exception e) {
            log.error("反序列化状态机上下文时发生未知错误", e);
            throw new RuntimeException("反序列化失败", e);
        } finally {
            input.close();
        }
    }

    /**
     * 状态机持久化
     */
    @Bean
    public DefaultStateMachinePersister<OrderStatesEnum, OrderEventsEnum, OrderInfo> stateMachinePersister(){
        return new DefaultStateMachinePersister<>(new StateMachinePersist<OrderStatesEnum, OrderEventsEnum, OrderInfo>() {
            @Override
            public void write(StateMachineContext<OrderStatesEnum, OrderEventsEnum> context, OrderInfo info) throws Exception {
                String key = REDIS_KEY_PREFIX + info.getId();
                try {
                    byte[] value = serialize(context);
                    log.info("正在写入任务 {} 的状态机上下文,状态为 {}", info.getId(), context.getState());
                    redisUtil.set(key, value);
                    log.info("任务 {} 的状态机上下文已成功写入Redis", info.getId());
                } catch (Exception e) {
                    log.error("写入任务 {} 的状态机上下文失败", info.getId(), e);
                    throw e;
                }
            }

            @Override
            public StateMachineContext<OrderStatesEnum, OrderEventsEnum> read(OrderInfo info) throws Exception {
                String key = REDIS_KEY_PREFIX + info.getId();
                log.info("正在读取任务 {} 的状态机上下文", info.getId());

                try {
                    byte[] value = (byte[]) redisUtil.get(key);
                    if (value == null) {
                        log.info("未找到任务 {} 的状态机上下文", info.getId());
                        return null;
                    }

                    StateMachineContext<OrderStatesEnum, OrderEventsEnum> context = deserialize(value);
                    if (context == null) {
                        log.error("反序列化任务 {} 的状态机上下文失败,可能需要重新初始化", info.getId());
                        // 清理损坏的数据
                        redisUtil.delete(key);
                        return null;
                    }

                    log.info("已从Redis读取任务 {} 的状态机上下文,状态为 {}", info.getId(), context.getState());
                    return context;
                } catch (Exception e) {
                    log.error("读取任务 {} 的状态机上下文时发生错误", info.getId(), e);
                    throw e;
                }
            }
        });
    }
}

4.3 编写状态机工具类

CustomStateMachineUtil.java

java 复制代码
import com.demo.enums.OrderStatesEnum;
import lombok.extern.slf4j.Slf4j;
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.Component;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

@Slf4j
@Component
public class CustomStateMachineUtil<S, E, T> {

    @Resource
    private StateMachine<S, E> orderStateMachine;
    @Resource
    private StateMachinePersister<S, E, T> orderStateMachinePersister;

    /**
     * 发送状态机事件的通用方法
     * @param currentState 当前状态
     * @param event 要发送的事件
     * @param info 消息实体
     */
    public synchronized void sendEvent(S currentState, E event, T info) {
        log.info("开始处理状态机事件: {}", info);

        try {
            // 启动状态机
            orderStateMachine.start();

            // 设置消息头,传递业务数据
            Map<String, Object> headers = new HashMap<>();
            headers.put("info", info);
            headers.put("startTime", System.currentTimeMillis());

            // 根据当前状态设置状态机状态
            log.info("当前状态: {}", currentState);

            // 方式一:从Redis恢复状态机状态
            orderStateMachinePersister.restore(orderStateMachine, info);
            // 方式二:将状态机的状态设置为业务对象的实际状态
            //        stateMachine.getStateMachineAccessor().doWithAllRegions(accessor -> {
            //            accessor.resetStateMachine(new org.springframework.statemachine.support.DefaultStateMachineContext<>(
            //                    currentState, null, null, null));
            //        });

            // 构建并发送消息
            Message<E> message = MessageBuilder
                    .withPayload(event)
                    .copyHeaders(headers)
                    .build();
            orderStateMachine.sendEvent(message);

            // 持久化状态机状态
            boolean persistSuccess = persist(info);
            if (!persistSuccess) {
                throw new RuntimeException("状态机持久化状态失败");
            }
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            log.error("状态机发送事件失败. 事件: {}, 异常: {}", info, e.getMessage(), e);
            throw new RuntimeException("状态机发送事件失败");
        } finally {
            if (Objects.nonNull(info)) {
                log.info("当前状态: {}", currentState);
                if (Arrays.asList(OrderStatesEnum.DONE, OrderStatesEnum.CANCELLED).contains(currentState)) {
                    log.info("已完成或已取消,停止状态机");
                    orderStateMachine.stop();
                }
            }
        }
    }

    /**
     * 持久化状态机状态
     * @param info  实体
     * @return      是否持久化成功
     */
    public synchronized boolean persist(T info) {
        try {
            log.info("持久化状态机开始,此时状态: {}", orderStateMachine.getState().getId());

            // 启用持久化:将状态机状态持久化到Redis
            log.info("持久化已启用,将状态机状态持久化到Redis");
            orderStateMachinePersister.persist(orderStateMachine, info);
            // 不启用持久化:跳过持久化步骤
//            log.info("持久化未启用,跳过持久化步骤");

            return true;
        } catch (Exception e) {
            log.error("持久化状态机状态失败. 异常: {}", e.getMessage(), e);
            return false;
        }
    }
}

4.4 修改业务调用

OrderServiceImpl.java

java 复制代码
import com.demo.common.stashmachine.util.CustomStateMachineUtil;
import com.demo.domain.OrderInfo;
import com.demo.enums.OrderEventsEnum;
import com.demo.enums.OrderStatesEnum;
import com.demo.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.persist.StateMachinePersister;
import org.springframework.statemachine.state.State;
import org.springframework.stereotype.Service;


@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private StateMachine<OrderStatesEnum, OrderEventsEnum> orderStateMachine;
    @Autowired
    private StateMachinePersister<OrderStatesEnum, OrderEventsEnum, OrderInfo> orderStateMachinePersister;
    @Autowired
    private CustomStateMachineUtil<OrderStatesEnum, OrderEventsEnum, OrderInfo> customStateMachineUtil;

    @Override
    public void payOrder(String orderId) {
        log.info("开始处理订单任务状态事件,orderId: {}", orderId);

        // 获取订单信息
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setId(orderId);
        orderInfo.setState(OrderStatesEnum.UNPAID);

        customStateMachineUtil.sendEvent(orderInfo.getState(), OrderEventsEnum.PAY, orderInfo);
    }

    @Override
    public void confirmReceive(String orderId) {
        log.info("开始处理确认收货状态事件,orderId: {}", orderId);

        // 获取订单信息
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setId(orderId);
        orderInfo.setState(OrderStatesEnum.WAITING_FOR_RECEIVE);

        customStateMachineUtil.sendEvent(orderInfo.getState(), OrderEventsEnum.RECEIVE, orderInfo);
    }

    @Override
    public void cancelOrder(String orderId, String reason) {
        log.info("开始处理取消订单状态事件,orderId: {},reason:{}", orderId, reason);

        // 获取订单信息
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setId(orderId);
        orderInfo.setState(OrderStatesEnum.UNPAID);
        orderInfo.setReason(reason);

        customStateMachineUtil.sendEvent(orderInfo.getState(), OrderEventsEnum.CANCEL, orderInfo);
    }

    @Override
    public OrderStatesEnum getCurrentState(String orderId) throws Exception {
        OrderInfo info = new OrderInfo();
        info.setId(orderId);
        // 从Redis恢复状态机状态
        orderStateMachinePersister.restore(orderStateMachine, info);
        State<OrderStatesEnum, OrderEventsEnum> state = orderStateMachine.getState();
        return state == null ? null : state.getId();
    }
}

4.5 测试结果

这次升级之后,再次调用接口可以发现,不同的订单编号已经可以分别存储不同的状态了,Redis 缓存内容如下:

整理完毕,完结撒花~🌻

参考地址:

1.SpringBoot集成spring-statemachine状态机实现业务流程,https://blog.csdn.net/weixin_37598243/article/details/140907763

2.spring-statemachine 状态机自定义持久化入库,https://blog.csdn.net/sjy_2010/article/details/133862831

3.SpringBoot集成Spring Statemachine(状态机)完整示例,https://juejin.cn/post/7441760738458779684

整理完毕,完结撒花~🌻

相关推荐
LSTM972 小时前
使用 Spire.XLS for Python 将 Excel 转换为 PDF
后端
Java微观世界2 小时前
Lombok、SpringBoot的底层逻辑:一文读懂Java注解的编译时与运行时处理
后端
考虑考虑2 小时前
Ubuntu服务器使用 Graphics2D 生成图片时出现文字乱码
运维·服务器·后端
开开心心就好2 小时前
电脑音质提升:杜比全景声安装详细教程
java·开发语言·前端·数据库·电脑·ruby·1024程序员节
yoi啃码磕了牙2 小时前
Unity—Localization 多语言
java·数据库·mysql
跟着珅聪学java2 小时前
在Java中判断Word文档中是否包含表格并读取表格内容,可以使用Apache POI库教程
java·开发语言·word
回家路上绕了弯3 小时前
内容平台核心工程:最热帖子排行实现与用户互动三元组存储查询
后端·微服务
Kuo-Teng3 小时前
LeetCode 73: Set Matrix Zeroes
java·算法·leetcode·职场和发展