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

文章目录

  • 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> {/*** 配置初始状态*/@Overridepublic 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*/@Overridepublic 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 {@Resourceprivate 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 {@Resourceprivate 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 {@Resourceprivate StateMachine<OrderState, OrderStatusChangeEvent> orderStateMachine;@Resourceprivate StateMachinePersister<OrderState, OrderStatusChangeEvent, Order> persister;@Overridepublic Order create() {Order order = new Order();order.setName("小明" + UUID.randomUUID());order.setStatus(OrderState.WAIT_PAYMENT.getValue());this.save(order);return order;}@Overridepublic 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);}@Overridepublic 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);}@Overridepublic 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);}@Overridepublic 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();@Overridepublic void write(StateMachineContext<Object, Object> context, Order order) throws Exception {map.put(order.getId(), context);}@Overridepublic 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.rhkb.cn/news/133169.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第31章_瑞萨MCU零基础入门系列教程之WIFI蓝牙模块驱动实验

本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写&#xff0c;需要的同学可以在这里获取&#xff1a; https://item.taobao.com/item.htm?id728461040949 配套资料获取&#xff1a;https://renesas-docs.100ask.net 瑞萨MCU零基础入门系列教程汇总&#xff1a; ht…

(LeetCode)两数相加深入分析Java版

两数相加&#xff08;题目如下&#xff09; 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。 你可以假设除了数…

【深度学习】Pytorch 系列教程(二):PyTorch数据结构:1、Tensor(张量): GPU加速(GPU Acceleration)

目录 一、前言 二、实验环境 三、PyTorch数据结构 0、分类 1、张量&#xff08;Tensor&#xff09; 1. 维度&#xff08;Dimensions&#xff09; 2. 数据类型&#xff08;Data Types&#xff09; 3. GPU加速&#xff08;GPU Acceleration&#xff09; 一、前言 ChatGP…

【Linux环境】基础开发工具的使用:yum软件安装、vim编辑器的使用

​&#x1f47b;内容专栏&#xff1a; Linux操作系统基础 &#x1f428;本文概括&#xff1a; yum软件包管理、vim编辑器的使用。 &#x1f43c;本文作者&#xff1a; 阿四啊 &#x1f438;发布时间&#xff1a;2023.9.12 Linux软件包管理 yum 什么是软件包 在Linux下安装软件…

【LeetCode-简单题】剑指 Offer 58 - II. 左旋转字符串

文章目录 题目方法一&#xff1a;连续双指针翻转 题目 方法一&#xff1a;连续双指针翻转 class Solution {public String reverseLeftWords(String s, int n) {StringBuffer sb new StringBuffer(s);reverseWord(sb,0,n-1);reverseWord(sb,n,sb.length()-1);return sb.revers…

WebServer 解析HTTP 响应报文

一、基础API部分&#xff0c;介绍stat、mmap、iovec、writev、va_list 1.1 stat​ 作用&#xff1a;获取文件信息 #include <sys/types.h> #include <sys/stat.h> #include <unistd.h>// 获取文件属性&#xff0c;存储在statbuf中 int stat(const char *…

CH06_第一组重构(上)

提取函数&#xff08;Extract Function |106&#xff09; 曾用名&#xff1a;提炼函数&#xff08;Extract Function&#xff09; 反向重构&#xff1a;内联函数&#xff08;115&#xff09; 示例代码 function printOwing(invoice) {printBanner();let outstanding calcul…

SpringBoot表现层数据一致性

1.定义Restful类 说明&#xff1a;使用Data注解是Lombok库提供的一个注解&#xff0c;用于自动生成类的getter、setter、equals、hashcode和toString方法。 package com.forever.controller.utils;import lombok.Data;Data public class Restful {private Boolean flag;//dat…

Redis 常用命令

目录 全局命令 1&#xff09;keys 2&#xff09;exists 3) del(delete) 4&#xff09;expire 5&#xff09;type SET命令 GET命令 MSET 和 MGET命令 其他SET命令 计数命令 redis-cli&#xff0c;进入redis 最核心的命令&#xff1a;我们这里只是先介绍 set 和 get 最简单的操作…

Java集合(Collection、Iterator、Map、Collections)概述——Java第十三讲

前言 本讲我们将继续来讲解Java的其他重要知识点——Java集合。Java集合框架是Java编程语言中一个重要的部分,它提供了一套预定义的类和接口,供程序员使用数据结构来存储和操作一组对象。Java集合框架主要包括两种类型:一种是集合(Collection),存储一个元素列表,…

MySQL入门指南:数据库操作的基础知识

当谈到关系型数据库管理系统(RDBMS)时&#xff0c;MySQL无疑是最常见和广泛使用的一个。它是一个强大的工具&#xff0c;用于存储、管理和检索数据。在这篇博客中&#xff0c;我们将介绍MySQL的基本知识&#xff0c;包括数据库的操作、数据表的操作以及数据的增删改查~~ 目录 …

嵌入式学习笔记(27)uart stdio的移植

什么是stdio&#xff1f; &#xff08;1&#xff09;#include <stdio.h> &#xff08;2&#xff09;stdio:standard input output &#xff08;3&#xff09;stdio是os定义的默认的输入和输出通道。一般在PC机的情况下&#xff0c;标准输入指的是键盘&#xff0c;标准…

VIOOVI干货分享:企业车间动作作业分析如何改善?

企业车间动作作业分析是将操作动作分解为最小的分析单位&#xff0c;我们通常称之为动素。通过对动素的定性研究&#xff0c;找出合理有效的动作&#xff0c;缩短操作时间&#xff0c;实现标准化操作。在实际应用中&#xff0c;应分析和研究操作员的各种动作&#xff0c;去除没…

深入探讨Kubernetes(K8s)在云原生架构中的关键作用和应用

文章目录 1. 容器化的应用程序管理2. 自动化扩展和负载均衡3. 容器编排和调度4. 存储管理5. 自动化滚动更新6. 多云和混合云部署7. 监控和日志8. 安全9. 社区支持和生态系统10. 未来展望案例 &#x1f388;个人主页&#xff1a;程序员 小侯 &#x1f390;CSDN新晋作者 &#x1…

Nginx部署前后端分离项目(Linux)

Nginx代理前端页面、后端接口 一、前端打包二、后端打包三、Linux部署Nginx启动、暂停、重启服务器部署文件地址&#xff1a; 一、前端打包 npm run build二、后端打包 通过Maven 使用package打包 三、Linux部署 安装Nginx 安装环境 yum -y install gcc pcre pcre-devel z…

SpringMVC系列(七)之自定义注解

目录 一. Java注解简介 1.1 Java注解分类 1.2 JDK基本注解 Override Deprecated SuppressWarnings 1.3 JDK元注解 从 Java 7 开始&#xff0c;额外添加了 3 个注解: 1.4 自定义注解 如何自定义注解&#xff1f; 二. 自定义注解示例 枚举类&#xff1a; 示例一&…

【JUC】Java并发编程从挖坑到入土全解(一)

目录 线程基础知识 作者&#xff08;拜个神&#xff09; 什么是JUC 为什么会出现多线程 硬件方面 摩尔定律失效 软件方面 弊端 & 问题 从线程启动开始 Java线程理解以及openJDK的实现 更加底层的的C源码 线程基础知识 作者&#xff08;拜个神&#xff09; Dou…

A Mathematical Framework for Transformer Circuits—(三)

A Mathematical Framework for Transformer Circuits Two-Layer Attention-Only TransformersThree Kinds of CompositionPath Expansion of LogitsPath Expansion of Attention Scores QK CircuitAnalyzing a Two-Layer ModelInduction HeadsInduction heads的功能Induction h…

CVE-2023-1454:Jeecg-Boot SQL注入漏洞复现

文章目录 Jeecg-Boot SQL注入漏洞(CVE-2023-1454)复现0x01 前言0x02 漏洞描述0x03 影响范围0x04 漏洞环境0x05 漏洞复现1.访问漏洞环境2.构造POC3.复现 0x06 修复建议 Jeecg-Boot SQL注入漏洞(CVE-2023-1454)复现 0x01 前言 免责声明&#xff1a;请勿利用文章内的相关技术从事…

Windows 性能突然打鸡血,靠 Bug 修复了多年顽疾

要说 的 Bug 集中地&#xff0c;当属资源管理器。 速度缓慢、卡顿、崩溃&#xff0c;不同设备、不同版本的用户都有不同的感受。 严格来说&#xff0c;这其实是 Windows 的传统艺能&#xff0c;要完美修复可不容易。 而作为小老弟的文件资源管理器&#xff0c;时不时来个无响…