状态管理艺术——借助Spring StateMachine驭服复杂应用逻辑

news2025/1/24 11:41:49

文章目录

  • 1. 什么是状态
  • 2. 有限状态机概述
  • 3. Spring StateMachine
  • 4. Spring StateMachine 入门小案例
    • 4.1 接口测试
  • 5. 总结

1. 什么是状态

在开发中,无时无刻离不开状态的一个概念,任何一条数据都有属于它的状态。

比如一个电商平台,一个订单会有很多状态,比如待付款、待发货、待收货、完成订单。而这其中每一个状态的改变都随着一个个事件的发生。比如将商品下单但未付款,那么订单就是待付款状态,当触发支付事件,那么订单就能从待付款状态转变未待发货状态,以此类推随之对应的事件就是发货、收货。

其二,状态的流动是固定了的。也就是说,待付款状态的下一个状态只能是待发货状态,不能直接转化为待收货状态。这种由待付款直接转变未待收货的状态是非法的,是程序不允许的。

对于这样的一种情况,最简单的解决方案无疑就是if-lese,比如编写一个支付接口,首先根据订单ID从数据库中查询出来订单信息,然后判断一下订单状态是不是待付款状态,如果是待付款状态,则可以继续下面的流程,否则抛出异常告知用户是非法操作。

image-20230910124440071

这种使用硬编码的if-else实现的效果固然没啥问题,但是如果中间状态出现了改变,比如待付款状态出现一个待拼单,那么代码改动幅度未免太大,难以维护。

这时候,学过设计模式的同学,很容易就想到了状态模式

状态模式将状态改变抽象成了三个角色:

  1. 环境角色(Context):也称上下文,定义了客户端需要的接口,维护一个当前状态,并将状态的相关操作委托给当前状态对象处理。
  2. 抽象状态角色(State):定义一个接口,用以封装环境对象中的特定状态所对应的行为。
  3. 具体状态(Concrete State)角色:实现抽象状态所对应的行为。

使用状态模式,可以将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。并且允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。

但是状态模式也存在缺点:

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

对比两种方案,状态模式是更好的解决方案,而对应到实践,也就是状态机。


2. 有限状态机概述

有限状态机(Finite-state machine,FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。

而要实现状态之间的流转,必须具备以下几个要素。

image-20230910130409427

1. 当前状态:状态流转的起始状态,如上图中的新建状态

2. 触发事件:引发状态与状态之间流转的事件,如上图中的创建订单这个动作

3. 响应函数:触发事件到下一个状态之间的规则

4. 目标状态:状态流转的终止状态,如上图中的待付款状态

简单来说,只有满足当订单是新建状态并且触发创建订单事件,才会执行触发函数,使得状态由新建转化为待付款。

这就是一个状态机的基本要素,但是要实现一个状态机并不简单,好在Spring为我们提供了Spring StateMachine框架。

3. Spring StateMachine

Spring Statemachine是应用程序开发人员在Spring应用程序中使用状态机概念的框架
Spring Statemachine旨在提供以下功能:

  1. 易于使用的扁平单级状态机,用于简单的使用案例。
  2. 分层状态机结构,以简化复杂的状态配置。
  3. 状态机区域提供更复杂的状态配置。
  4. 使用触发器,转换,警卫和操作。
  5. 键入安全配置适配器。
  6. 生成器模式,用于在Spring Application上下文之外使用的简单实例化通常用例的食谱
  7. 基于Zookeeper的分布式状态机
  8. 状态机事件监听器。
  9. UML Eclipse Papyrus建模。
  10. 将计算机配置存储在永久存储中。
  11. Spring IOC集成将bean与状态机关联起来。

官网:spring.io/projects/sp…

源码:github.com/spring-proj…

API:docs.spring.io/spring-stat…

状态机是一种用于控制应用程序状态转换的机制。它包含了一组预定义的状态和状态之间的转换规则。在应用程序运行时,通过不同的事件或计时器触发,状态机能够根据事先定义好的规则自动地改变应用程序的状态。这种设计思想使得开发人员能够更加方便地追踪和调试应用程序的行为,因为状态转换的规则是在启动时确定的,而不需要动态地修改或推断。


4. Spring StateMachine 入门小案例

首先,引入Spring StateMachine 的依赖。

<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-core</artifactId>
    <version>2.1.3.RELEASE</version>
</dependency>

定义订单状态的枚举与触发订单状态改变的事件枚举

/**
 * @description: 订单状态
 * @author:lrk
 * @date: 2023/9/6
 */
@AllArgsConstructor
@Getter
public enum OrderState {

    WAIT_PAYMENT(1, "待支付"),
    WAIT_DELIVER(2, "待发货"),
    WAIT_RECEIVE(3, "待收货"),
    FINISH(4, "已完成");
    private Integer value;
    private String desc;
}
/**
 * @description: 事件枚举类
 * @author:lrk
 * @date: 2023/9/6
 */
public enum OrderStatusChangeEvent {
    /**
     * 支付
     */
    PAYED,

    /**
     * 发货
     */
    DELIVERY,

    /**
     *  确认收货
     */
    RECEIVED
}

创建一个订单表,这里只是简单演示,所有只有id、用户名称和订单状态

CREATE TABLE `t_order`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '下单用户名称',
  `status` tinyint NULL DEFAULT NULL COMMENT '订单状态(1:待支付,2:待发货,3:待收货,4:已完成)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

接着,编写状态机的配置类。

  1. 绑定初始状态与解决状态,以及所有的订单状态
  2. 绑定从一个状态流向下一个状态需要触发的事件
/**
 * @description: 状态机配置类
 * @author:lrk
 * @date: 2023/9/6
 */
@Configuration
@EnableStateMachine(name = "orderStateMachine")
@Slf4j
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderState, OrderStatusChangeEvent> {

    /**
     * 配置初始状态
     */
    @Override
    public void configure(StateMachineStateConfigurer<OrderState, OrderStatusChangeEvent> states) throws Exception {
        states.withStates()
                // 指定初始化状态
                .initial(OrderState.WAIT_PAYMENT)
            	// 指定解决状态
                .end(OrderState.FINISH)
                .states(EnumSet.allOf(OrderState.class));
    }



    /**
     * 配置状态转换事件关系
     *
     * @param transitions
     * @throws Exception
     */
    @Override
    public void configure(StateMachineTransitionConfigurer<OrderState, OrderStatusChangeEvent> transitions) throws Exception {
        transitions
                //支付事件:待支付-》待发货
                .withExternal().source(OrderState.WAIT_PAYMENT).target(OrderState.WAIT_DELIVER)
                .event(OrderStatusChangeEvent.PAYED)
                .and()
                //发货事件:待发货-》待收货
                .withExternal().source(OrderState.WAIT_DELIVER).target(OrderState.WAIT_RECEIVE)
                .event(OrderStatusChangeEvent.DELIVERY)
                .and()
                //收货事件:待收货-》已完成
                .withExternal().source(OrderState.WAIT_RECEIVE).target(OrderState.FINISH).event(OrderStatusChangeEvent.RECEIVED);
    }
}

接着,编写状态机监听器。

状态机监听器种指定了状态从某个状态到某个状态的时候会触发哪个方法,执行方法的逻辑。

比如订单状态一开始是WAIT_PAYMENT,需要转化为WAIT_DELIVER

那么就会执行payTransition方法的逻辑,在这个方法中可以编写相应的业务逻辑。

/**
 * @description: 状态机监听器
 * @author:lrk
 * @date: 2023/9/6
 */
@WithStateMachine(name = "orderStateMachine")
@Slf4j
@Component("orderStateListener")
public class OrderListener {

    @Resource
    private OrderService orderService;


    @OnTransition(source = "WAIT_PAYMENT", target = "WAIT_DELIVER")
    public boolean payTransition(Message<OrderStatusChangeEvent> message) {
        Order order = (Order) message.getHeaders().get("order");
        order.setStatus(OrderState.WAIT_DELIVER.getValue());
        log.info("支付,状态机反馈信息:" + message.getHeaders().toString());
        return orderService.updateById(order);
    }

    @OnTransition(source = "WAIT_DELIVER", target = "WAIT_RECEIVE")
    public boolean deliverTransition(Message<OrderStatusChangeEvent> message) {
        Order order = (Order) message.getHeaders().get("order");
        order.setStatus(OrderState.WAIT_RECEIVE.getValue());
        log.info("发货,状态机反馈信息:" + message.getHeaders().toString());
        return orderService.updateById(order);
    }

    @OnTransition(source = "WAIT_RECEIVE", target = "FINISH")
    public boolean receiveTransition(Message<OrderStatusChangeEvent> message) {
        Order order = (Order) message.getHeaders().get("order");
        order.setStatus(OrderState.FINISH.getValue());
        log.info("收货,状态机反馈信息:" + message.getHeaders().toString());
        return orderService.updateById(order);
    }
}

接着编写接口

/**
 * @description: 订单接口
 * @author:lrk
 * @date: 2023/9/6
 */
@RestController
@RequestMapping("order")
public class OrderController {

    @Resource
    private OrderService orderService;

    @GetMapping("create")
    public BaseResponse<Order> create() {
        return ResultUtils.success(orderService.create());
    }

    @GetMapping("pay")
    public BaseResponse<Order> pay(@RequestParam Integer id) {
        return ResultUtils.success(orderService.pay(id));
    }

    @GetMapping("deliver")
    public BaseResponse<Order> deliver(@RequestParam Integer id) {
        return ResultUtils.success(orderService.deliver(id));
    }

    @GetMapping("receive")
    public BaseResponse<Order> receive(@RequestParam Integer id) {
        return ResultUtils.success(orderService.receive(id));
    }


    @GetMapping("getOrders")
    public BaseResponse<List<Order>> getOrders() {
        return ResultUtils.success(orderService.getOrders());
    }
}
/**
 * @author lrk
 * @description 针对表【t_order】的数据库操作Service实现
 * @createDate 2023-09-06 22:42:22
 */
@Service
@Slf4j
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order>
        implements OrderService {

    @Resource
    private StateMachine<OrderState, OrderStatusChangeEvent> orderStateMachine;

    @Resource
    private StateMachinePersister<OrderState, OrderStatusChangeEvent, Order> persister;


    @Override
    public Order create() {
        Order order = new Order();
        order.setName("小明" + UUID.randomUUID());
        order.setStatus(OrderState.WAIT_PAYMENT.getValue());
        this.save(order);
        return order;
    }

    @Override
    public Order pay(int id) {
        Order order = this.getById(id);
        log.info("支付:order订单信息:{}", order);
        if (!sendEvent(OrderStatusChangeEvent.PAYED, order)) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "状态转换异常");
        }
        return this.getById(id);
    }

    @Override
    public Order deliver(int id) {
        Order order = this.getById(id);
        log.info("发货:order订单信息:{}", order);
        if (!sendEvent(OrderStatusChangeEvent.DELIVERY, order)) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "状态转换异常");
        }
        return this.getById(id);
    }

    @Override
    public Order receive(int id) {
        Order order = this.getById(id);
        log.info("收货:order订单信息:{}", order);
        if (!sendEvent(OrderStatusChangeEvent.RECEIVED, order)) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "状态转换异常");
        }
        return this.getById(id);
    }

    @Override
    public List<Order> getOrders() {
        return this.list();
    }


    /**
     * 发送订单状态转换事件
     * synchronized修饰保证这个方法是线程安全的
     *
     * @param changeEvent
     * @param order
     * @return
     */
    private synchronized boolean sendEvent(OrderStatusChangeEvent changeEvent, Order order) {
        boolean result = false;
        try {
            //启动状态机
            orderStateMachine.start();
            //尝试恢复状态机状态
            persister.restore(orderStateMachine, order);
            Message message = MessageBuilder.withPayload(changeEvent).setHeader("order", order).build();
            result = orderStateMachine.sendEvent(message);
            //持久化状态机状态
            persister.persist(orderStateMachine, order);
        } catch (Exception e) {
            log.error("订单操作失败:{}", e);
        } finally {
            orderStateMachine.stop();
        }
        return result;

    }
}

其实到这,还需要思考一个问题,在业务层通过状态机发送的只是订单转变事件只是订单状态改变的事件OrderStatusChangeEvent,那么状态机怎么知道初始状态是什么?因为需要靠初始状态判断是否达到体检可以转变状态。

这就需要配置状态机持久化配置了

/**
 * 持久化配置
 * 实际使用中,可以配合redis等,进行持久化操作
 *
 * @return
 */
@Bean
public DefaultStateMachinePersister persister() {
    return new DefaultStateMachinePersister<>(new StateMachinePersist<Object, Object, Order>() {
        //这个内存中的示例仅用于演示目的。对于真正的应用程序,你应该使用真正的持久存储实现。
        private Map<Long, StateMachineContext<Object, Object>> map = new HashMap();

        @Override
        public void write(StateMachineContext<Object, Object> context, Order order) throws Exception {
            map.put(order.getId(), context);
        }

        @Override
        public StateMachineContext<Object, Object> read(Order order) throws Exception {
            return map.get(order.getId());
        }
    });
}

首先状态机会触发read(Order order)方法,在持久化存储中读取相应的状态机上下文。

这样状态机就能获取到的初始状态了。

write(StateMachineContext<Object, Object> context, Order order)方法,则是将订单ID对应的上下文放到map集合中去。

根据订单的初始状态和触发事件对应的目标状态,执行相对应的状态机监听器事件。

然后将状态机修改后的订单状态的上下文通过write方法,写进map中,以便下一次订单状态流转的时候可以用到。


4.1 接口测试

一开始,创建一个订单,订单状态为1,也就是待付款。

image-20230910140018214

接着调用支付接口,触发支付事件,订单状态流转为2,也就是待发货

image-20230910140113795

如果这时候,不调用发货接口,直接调用收货接口,订单状态会不会改变呢?

image-20230910140200977

很明显不会,状态机会识别到状态流转异常,在sendEvent会返回false表示失败,接着业务层抛出异常。

继续调用发货接口,订单触发发货事件,订单状态转变为3,也就是待收货状态。

image-20230910140344843

最后,收货,整个订单状态流转过程就完美完成了!

image-20230910140412868


5. 总结

Spring StateMachine是Spring旗下的一个状态机框架。所以生态非常丰富,与Spring整合度非常高,非常适合结合Spring框架去使用。

但是,Spring StateMachine定制性难度困难,因为Spring StateMachine是一个复杂的框架,各方面来说难以定制化。

所以如果是直接使用状态机的组件库,可以考虑使用Spring的状态机。


参考

  1. Squirrel状态机-从原理探究到最佳实践 - 掘金 (juejin.cn)
  2. 状态机的介绍和使用 | 京东物流技术团队 - 掘金 (juejin.cn)
  3. Spring之状态机讲解_spring状态机_爱吃牛肉的大老虎的博客-CSDN博客
  4. Spring StateMachine 文档 | 中文文档 (gitcode.host)
  5. 【设计模式】软件设计原则以及23种设计模式总结_起名方面没有灵感的博客-CSDN博客
  6. 使用Spring StateMachine框架实现状态机 (taodudu.cc)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/996133.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

自动化测试的重要性:为何追求自动化?

为什么需要自动化测试&#xff1f; 代替手工重复操作&#xff0c;测试工程师可以花更多时间在设计全面的测试用例和新功能测试上 【代替手工重复】 提升回归测试的效率&#xff0c;适合敏捷开发过程 【提升回归效率】 更好的利用非工作时间执行测试&#xff0c;工作时间分析失…

油猴浏览器(安卓)

油猴浏览器页面设计非常简约&#xff0c;在主页上还为小伙伴们推荐了很多的常用书签&#xff0c;像油猴脚本&#xff0c;常用导航&#xff0c;新闻&#xff0c;热搜类的&#xff0c;快递查询等等&#xff0c;可以设置快捷访问&#xff0c;把常用到的一些网站设置在主页上。 浏览…

代码随想录Day_60打卡

①、柱状图中最大的矩形 给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的高度。每个柱子彼此相邻&#xff0c;且宽度为 1 。 求在该柱状图中&#xff0c;能够勾勒出来的矩形的最大面积。 事例&#xff1a; 输入&#xff1a;heights [2,1,5,6,2,3] 输出&#xff1a;1…

Chrome 117 发布:新 Web 开发调试技巧都在这了!

简介&#xff1a;Chrome 更新了最新版本 Chrome 117&#xff0c;更新了很多实用的DevTools 新特性。 首先介绍大家最熟悉的Network面板&#xff0c;看看给我们带来了什么好玩的~ Network 面板改进 更快地在本地覆盖网页内容 现在&#xff0c;本地覆盖功能更加简化&#xff0…

【数据结构】二叉树基础入门

&#x1f490; &#x1f338; &#x1f337; &#x1f340; &#x1f339; &#x1f33b; &#x1f33a; &#x1f341; &#x1f343; &#x1f342; &#x1f33f; &#x1f344;&#x1f35d; &#x1f35b; &#x1f364; &#x1f4c3;个人主页 &#xff1a;阿然成长日记 …

Alibaba(商品详情)API接口

为了进行电商平台 的API开发&#xff0c;首先我们需要做下面几件事情。 1&#xff09;开发者注册一个账号 2&#xff09;然后为每个alibaba应用注册一个应用程序键&#xff08;App Key) 。 3&#xff09;下载alibaba API的SDK并掌握基本的API基础知识和调用 4&#xff09;利…

解锁“高级特权” 偷偷用。千万别声张

现在的手机修图软件&#xff0c;功能是越来越强大了。之前需要用到电脑端PS的功能&#xff0c;现在手机上的修图软件就能实现&#xff0c;还是一键处理的傻瓜操作。 你离P图大神只差一个APP——「大神批图」&#xff0c;已解锁限制&#xff0c;安装即是VIP。 功能非常丰富&…

多元共进|创新技术提供助力,共创增长机遇

谷歌致力于推动业务和应用长效增长 助力开发者优化用户体验 一起来了解 2023 Google 开发者大会上 谷歌如何将创新技术 融入商业合作和智能家居生态 用技术赋能业务增长 以科技点亮智慧生活 谷歌坚持以 AI 为技术核心&#xff0c;不断投入研究&#xff0c;并将其应用至各类场景…

【C++刷题】经典简单题第二辑

回文排列 class Solution { public:bool canPermutePalindrome(string s) {// 记录字符出现的次数int count[256] {0};for(size_t i 0; i < s.size(); i)count[s[i]];// 记录字符出现次数为奇数的个数int flag 0;for(size_t i 0; i < 256; i)if(count[i] % 2 1)f…

JavaScript作用域链与预解析

查找上一级 JavaScript的预解析 js解析器在运行js代码时会先进行预解析,再进行代码的执行 预解析时js引擎会把js里面所有的var还有function提升到当前作用域的最前面 代码执行,按照代码书写的顺序从上往下执行 预解析分为变量预解析(变量提升)与函数预解析(函数提升) 1.变量…

优秀的 Modbus 主站(主机、客户端)仿真器、串口调试工具

文章目录 优秀的 Modbus 主站&#xff08;主机、客户端&#xff09;仿真器、串口调试工具主要功能软件截图 优秀的 Modbus 主站&#xff08;主机、客户端&#xff09;仿真器、串口调试工具 modbus master,modbus,串口,工控,物联网,PLC,嵌入式 官网下载地址&#xff1a;http:/…

七)Stable Diffussion使用教程:附加功能

图生图右侧有个附加功能选项,里面其实也存在一个图片放大(缩放)功能,而且因为它不涉及重绘的过程,所以不需要任何提示词,适用于任何图片(包括非 SD 生成的图片)。 原理:在拉伸放大的基础上适当对色块和线条的边缘做了模糊处理,和其他工具的放大原理类似。 观察界面…

【LeetCode-中等题】367. 有效的完全平方数

文章目录 题目方法一&#xff1a;二分查找 题目 方法一&#xff1a;二分查找 找 1 - num 之间的 mid&#xff0c; 开方是整数 就找得到 mid&#xff0c; 不是整数自然找不到mid class Solution { // 二分查找 &#xff1b;找 1 - num 之间的mid 开方是整数 就找得到 不是…

python超详细安装

目录 初始python获取python安装包python解释器安装pycharm编译器安装pycharm的简单使用&#xff08;第一个hello world&#xff09; 初始python Python 是一款易于学习且功能强大的编程语言。 它具有高效率的数据结构&#xff0c;能够简单又有效地实现面向对象编程。 Python简…

平衡二叉搜索树(AVL)——【C++实现插入、删除等操作】

本章完整代码gitee地址&#xff1a;平衡二叉搜索树 文章目录 &#x1f333;0. 前言&#x1f332;1. AVL树概念&#x1f334;2. 实现AVL树&#x1f33f;2.1 结构定义&#x1f33f;2.2 插入&#x1f490;左单旋&#x1f490;右单旋&#x1f490;左右双旋&#x1f490;右左双旋 &a…

c++day1

练习&#xff1a;使用cout完成输出斐波那契前20项的内容 1 1 2 3 5 8 13.。。。 #include <iostream> using namespace std;int main() {int a[20]{1,1};for(int i2;i<20;i){a[i]a[i-1]a[i-2];}for(int i0;i<20;i){cout<<a[i]<<" ";}retur…

javascript【格式化时间日期】

javascript【格式化时间日期】 操作&#xff1a; (1) 日期格式化代码 /*** 日期格式化函数<br/>* 调用格式&#xff1a;需要使用日期对象调用* <p> new Date().Format("yyyy/MM/dd HH:mm:ss"); </p>* param fmt 日期格式* returns {*} 返回格式化…

易优cms响应式月嫂家政服务公司网站模板源码—自适应手机端设计,支持后台管理

易优cms响应式月嫂家政服务公司网站模板源码 自适应手机端 带后台 模板基于EyouCMS内核制作,模板编码为UTF8 ,适合行业:家政服务类企业。 模板信息&#xff1a; 模板分类&#xff1a;摄像、婚庆、家政、保洁 适合行业&#xff1a;家政服务类企业 模板介绍&#xff1a; 本模…

龙蜥Anolis 8.8 安装MySQL

一、安装参考文档 官方文档&#xff1a;https://dev.mysql.com/doc/refman/8.0/en/linux-installation-yum-repo.html#yum-repo-installing-mysql出问题时的文档&#xff1a;https://blog.csdn.net/weixin_44798320/article/details/123446249 二、安装过程 2.1 下载官方的镜…

RocketMQ入门之学习环境搭建

文章目录 0.前言1.使用docker 方式搭建RocketMQ学习环境启动NameServer和 启动Broker常见报错 2.使用源码安装方式3. 常见问题3. 参考文档 0.前言 在学习RocketMQ 需要先自行搭建一个RocketMQ 服务端。本节我们先来搭建一个简单的学习环境。下个章节&#xff0c;我们写个简单的…