玩转 Spring 状态机:更优雅的实现订单状态流转

news2024/11/15 11:17:45

说起 Spring 状态机,大家很容易联想到这个状态机和设计模式中状态模式的区别是啥呢?没错,Spring 状态机就是状态模式的一种实现,在介绍 Spring 状态机之前,让我们来看看设计模式中的状态模式。

1. 状态模式

状态模式的定义如下:

状态模式(State Pattern)是一种行为型设计模式,它允许对象在内部状态发生变化时改变其行为。在状态模式中,一个对象的行为取决于其当前状态,而且可以随时改变这个状态。状态模式将对象的状态封装在不同的状态类中,从而使代码更加清晰和易于维护。当一个对象的状态改变时,状态模式会自动更新该对象的行为,而不需要在代码中手动进行判断和处理。

通常业务系统中会存在一些拥有状态的对象,而且这些状态之间可以进行转换,并且在不同的状态下会表现出不同的行为或者不同的功能,比如交通灯控制系统中会存在红灯、绿灯和黄灯,再比如订单系统中的订单会存在已下单、待支付、待发货、待收货等状态,这些状态会通过不同的行为进行相互转换,这时候在系统设计时就可以使用状态模式。

下面是状态模式的类图:

图片

可以看到状态模式主要包含三种类型的角色:

1、上下文 (Context) 角色: 封装了状态的实例,负责维护状态实例,并将请求委托给当前的状态对象。

2、抽象状态 (State) 角色: 定义了表示不同状态的接口,并封装了该状态下的行为。所有具体状态都实现这个接口。

3、具体状态 (Concrete State) 角色: 具体实现了抽象状态角色的接口,并封装了该状态下的行为。

下面是使用状态模式实现红绿灯状态变更的一个简单案例:

抽象状态类:

/**
 * @description: 抽象状态类
 */
public abstract class MyState {
    abstract void handler();
}

具体状态类 A

/**
 * @description: 具体状态A
 */
public class RedLightState extends MyState{

    @Override
    void handler() {
        System.out.println("红灯停");
    }
}

具体状态类 B

/**
 * @description: 具体状态B
 */
public class GreenLightState extends MyState{

    @Override
    void handler() {
        System.out.println("绿灯行");
    }
}

环境类:维护当前状态对象,并提供了切换状态的方法。

/**
 * @description: 环境类
 */
public class MyContext {

    private MyState state;

    public void setState(MyState state) {
        this.state = state;
    }

    public void handler() {
        state.handler();
    }
}

测试类

/**
 * @description: 测试状态模式
 */
public class TestStateModel {
    public static void main(String[] args) {
        MyContext myContext = new MyContext();

        RedLightState redLightState = new RedLightState();
        GreenLightState greenLightState = new GreenLightState();

        myContext.setState(redLightState);
        myContext.handler(); //红灯停

        myContext.setState(greenLightState);
        myContext.handler(); //绿灯行
    }
}

下面是对应的执行结果

图片

可以发现,使用状态模式中的状态类在一定程度上也消除了 if-else 逻辑校验,看到这里, 有些人可能会有疑问:状态模式和策略模式的区别是什么呢?

状态模式更关注对象在不同状态的行为和状态之间的流转,而策略模式更关注对象不同策略的选择。

上面我们介绍了设计模式中的状态模式,接下来我们来看看 Spring 状态机。

2. Spring 状态机

状态机,也就是 State Machine ,不是指一台实际机器,而是指一个数学模型 。说白了,就是指一张状态转换图。 状态机是状态模式的一种应用,相当于上下文角色的一个升级版。在工作流或游戏等各种系统中有大量使用,如各种工作流引擎,它几乎是状态机的子集和实现,封装状态的变化规则。Spring 也提供了一个很好的解决方案。Spring 中的组件名称就叫作状态机(StateMachine)。状态机帮助开发者简化状态控制的开发过程,让状态机结构更加层次化。

通过定义,我们很容易分析得到状态机应当具备一下几个要素:

1. 当前状态: 也就是状态流转的起始状态。

2. 触发事件: 引起状态之间流转的一些列动作。

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

4. 目标状态: 状态流转的目标状态。

对于组件化的状态机,当前使用较多的主要是两种:一种是 Spring 状态机,一种是 COLA 状态机,这两种状态机的对比如下表所示:

图片

可以看到,Spring 状态机锁提供的内容较为丰富,当然对于自定义的支持就不如 COLA 状态机好,如果对自定义的需求比较高,那建议使用 COLA 状态机。

本文以 Spring 状态机为例,展示如何在业务系统中使用状态机。

为了便于大家了解 Spring 状态机的实现原理和使用方式以及其提供的功能,下面列出了官方文档和源码,感兴趣的同学可以阅读阅读。

官方文档:

  • https://docs.spring.io/spring-statemachine/docs/4.0.0/reference/index.html#statemachine-config-states

源代码:

  • https://github.com/spring-projects/spring-statemachine

3. Spring 状态机实现订单状态流转

对于状态模式,Spring 封装好了一个组件,就叫状态机(StateMachine)。Spring 状态机可以帮助我们开发者简化状态控制的开发过程,让状态机结构更加层次化。下面用 Spring 状态机模拟一个订单状态流转的过程。

3.1 环境准备

首先,如果要使用 spring 状态机,需要引入对应的 jar 包,这里我的 springboot 版本是:2.2.1.RELEASE

<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-core</artifactId>
    <version>${springboot.version}</version>
</dependency>

下面是简化的订单的定义,以及订单状态和订单转换行为的枚举

/**
 * @description: 模拟订单类
 */
@Data
public class Order {
    private Long orderId;
    private OrderStatusEnum orderStatus;
}

/**
 * @description: 订单状态
 */
public enum OrderStatusEnum {
    // 待支付
    WAIT_PAYMENT,
    // 待发货
    WAIT_DELIVER,
    // 待收货
    WAIT_RECEIVE,
    // 完成
    FINISH;
}

/**
 * @description:订单状态转换行为
 */
public enum OrderStatusChangeEventEnum {
    //支付
    PAYED,
    //发货
    DELIVERY,
    //收货
    RECEIVED;
}
3.2 构造订单状态机

在引入 jar 包之后,需要构建一个针对订单状态流转的状态机

订单状态机配置类如下:

/**
 * @description: 订单状态机
 */
@Configuration
@EnableStateMachine
public class OrderStatusMachineConfig extends StateMachineConfigurerAdapter<OrderStatusEnum, OrderStatusChangeEventEnum> {

    /**
     * 配置状态
     */
    @Override
    public void configure(StateMachineStateConfigurer<OrderStatusEnum, OrderStatusChangeEventEnum> states) throws Exception {
        states.withStates()
                .initial(OrderStatusEnum.WAIT_PAYMENT)
                .end(OrderStatusEnum.FINISH)
                .states(EnumSet.allOf(OrderStatusEnum.class));
    }

    /**
     * 配置状态转换事件关系
     */
    @Override
    public void configure(StateMachineTransitionConfigurer<OrderStatusEnum, OrderStatusChangeEventEnum> transitions) throws Exception {
        transitions.withExternal().source(OrderStatusEnum.WAIT_PAYMENT).target(OrderStatusEnum.WAIT_DELIVER)
                .event(OrderStatusChangeEventEnum.PAYED)
                .and()
                .withExternal().source(OrderStatusEnum.WAIT_DELIVER).target(OrderStatusEnum.WAIT_RECEIVE)
                .event(OrderStatusChangeEventEnum.DELIVERY)
                .and()
                .withExternal().source(OrderStatusEnum.WAIT_RECEIVE).target(OrderStatusEnum.FINISH)
                .event(OrderStatusChangeEventEnum.RECEIVED);
    }
}
3.3 编写状态机监听器

监听状态变更事件,完成状态转换。

/**
 * @description: 状态监听
 */
@Component
@WithStateMachine
@Transactional
public class OrderStatusListener {
    @OnTransition(source = "WAIT_PAYMENT", target = "WAIT_DELIVER")
    public boolean payTransition(Message message) {
        Order order = (Order) message.getHeaders().get("order");
        order.setOrderStatus(OrderStatusEnum.WAIT_DELIVER);
        System.out.println("支付,状态机反馈信息:" + message.getHeaders().toString());
        return true;
    }

    @OnTransition(source = "WAIT_DELIVER", target = "WAIT_RECEIVE")
    public boolean deliverTransition(Message message) {
        Order order = (Order) message.getHeaders().get("order");
        order.setOrderStatus(OrderStatusEnum.WAIT_RECEIVE);
        System.out.println("发货,状态机反馈信息:" + message.getHeaders().toString());
        return true;
    }

    @OnTransition(source = "WAIT_RECEIVE", target = "FINISH")
    public boolean receiveTransition(Message message) {
        Order order = (Order) message.getHeaders().get("order");
        order.setOrderStatus(OrderStatusEnum.FINISH);
        System.out.println("收货,状态机反馈信息:" + message.getHeaders().toString());
        return true;
    }

}
3.4 编写订单服务类

模拟对订单的一些业务操作

/**
 * @description: 订单服务
 */
@Service
public class OrderServiceImpl implements OrderService {

    @Resource
    private StateMachine<OrderStatusEnum, OrderStatusChangeEventEnum> orderStateMachine;

    private long id = 1L;

    private Map<Long, Order> orders = Maps.newConcurrentMap();

    @Override
    public Order create() {
        Order order = new Order();
        order.setOrderStatus(OrderStatusEnum.WAIT_PAYMENT);
        order.setOrderId(id++);
        orders.put(order.getOrderId(), order);
        System.out.println("订单创建成功:" + order.toString());
        return order;
    }

    @Override
    public Order pay(long id) {
        Order order = orders.get(id);
        System.out.println("尝试支付,订单号:" + id);
        Message message = MessageBuilder.withPayload(OrderStatusChangeEventEnum.PAYED).
                setHeader("order", order).build();
        if (!sendEvent(message)) {
            System.out.println(" 支付失败, 状态异常,订单号:" + id);
        }
        return orders.get(id);
    }

    @Override
    public Order deliver(long id) {
        Order order = orders.get(id);
        System.out.println(" 尝试发货,订单号:" + id);
        if (!sendEvent(MessageBuilder.withPayload(OrderStatusChangeEventEnum.DELIVERY)
                .setHeader("order", order).build())) {
            System.out.println(" 发货失败,状态异常,订单号:" + id);
        }
        return orders.get(id);
    }

    @Override
    public Order receive(long id) {
        Order order = orders.get(id);
        System.out.println(" 尝试收货,订单号:" + id);
        if (!sendEvent(MessageBuilder.withPayload(OrderStatusChangeEventEnum.RECEIVED)
                .setHeader("order", order).build())) {
            System.out.println(" 收货失败,状态异常,订单号:" + id);
        }
        return orders.get(id);
    }


    @Override
    public Map<Long, Order> getOrders() {
        return orders;
    }

    /**
     * 发送状态转换事件
     * @param message
     * @return
     */
    private synchronized boolean sendEvent(Message<OrderStatusChangeEventEnum> message) {
        boolean result = false;
        try {
            orderStateMachine.start();
            result = orderStateMachine.sendEvent(message);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (Objects.nonNull(message)) {
                Order order = (Order) message.getHeaders().get("order");
                if (Objects.nonNull(order) && Objects.equals(order.getOrderStatus(), OrderStatusEnum.FINISH)) {
                    orderStateMachine.stop();
                }
            }
        }
        return result;
    }
}
3.5 测试入口

这里编写一个 controller 模拟 c 端用户请求,为了便于展示,这里使用一个测试方法完成所有的操作

@RestController
public class OrderController {

    @Resource
    private OrderService orderService;

    @RequestMapping("/testOrderStatusChange")
    public String testOrderStatusChange(){
        orderService.create();
        orderService.create();
        orderService.pay(1L);
        orderService.deliver(1L);
        orderService.receive(1L);
        orderService.pay(2L);
        orderService.deliver(2L);
        orderService.receive(2L);
        System.out.println("全部订单状态:" + orderService.getOrders());
        return "success";
    }

}

下面是对应的执行结果

图片

可以看到 spring 状态机很好的控制了订单在各个状态之间的流转。

4. 思考与总结

思考:针对状态机的特点,还有其他思路实现一个状态机吗?下面是一些常规思路,如果还有其他方法欢迎在评论区留言。

  1. 消息队列方式

订单状态的流转可以通过 MQ 发布一个事件,消费者根据业务条件把订单状态进行流转,可以根据不同的事件发送到不同的 Topic。

  1. 定时任务驱动

每隔一段时间启动一下 job,根据特定的状态从数据库中拿对应的订单记录,然后判断订单是否有条件到达下一个状态。

  1. 规则引擎方式

业务团队可以在规则引擎里编写一系列的状态及其对应的转换规则,由规则引擎根据已经加载的规则对输入数据进行解析,根据解析的结果执行相应的动作,完成状态流转。

总结:

本文主要介绍了设计模式中的状态模式,并在此基础上介绍了 Spring 状态机相关的概念,并根据常见的订单流转场景,介绍了 Spring 状态机的使用方式。文中如有不当之处,欢迎在评论区批评指正。

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

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

相关文章

leetcode刷题(javaScript)——堆相关场景题总结

堆是什么&#xff1f;堆都能用树表示&#xff0c;并且一般树的实现都是利用链表。平时使用的最多的是二叉堆&#xff0c;它可以用完全二叉树表示&#xff0c;二叉堆易于存储&#xff0c;并且便于索引。在堆的实现时注意&#xff1a;因为是数组&#xff0c;所以父子节点的关系就…

学习SSM的记录(六)-- Spring MVC

目录 Spring MVC 简介和体验 Spring MVC原理简单解析 Spring MVC涉及的组件 Spring MVC 快速体验 Spring MVC 接收数据 访问路径设置 接收参数&#xff08;重点&#xff09; param和json参数比较 param参数接收 路径参数接收 json参数接收 EnableWebMvc注解 接收C…

探秘知乎的排名算法:知乎撰写高质量内容的秘诀

知乎作为一个知识问答社区&#xff0c;用户众多、内容繁杂&#xff0c;那么究竟是什么样的原则决定了知乎上的排名呢&#xff1f;腾轩科技传媒探讨知乎排名的规则&#xff0c;并分享如何撰写高质量的文章。 知乎排名的算法 在知乎这个巨大的社交平台上&#xff0c;任何一个用户…

DDL - 建立数据库,建表代码版(Way 2)

一、DB操作 show databases; create database DBOFRYX; drop database DBOFRYX; use DBOFRYX; 二、表操作&#xff08;表和表结构、字段是A、B两姐妹&#xff09; (1) use DBOFRYX; show tables; (2) create table TABOFRYX( name varchar(50) comment "姓名"…

ChatGPT提问技巧——对话提示

ChatGPT提问技巧——对话提示 对话提示是一种允许模型生成模拟两个或多个实体之间对话的文本的技术。 通过向模型提供上下文和一组角色或实体&#xff0c;以及他们的角色和背景&#xff0c;并要求模型生成他们之间的对话。 因此&#xff0c;应向模型提供一个上下文和一组角色…

大语言模型智能体简介

大语言模型&#xff08;LLM&#xff09;智能体&#xff0c;是一种利用大语言模型进行复杂任务执行的应用。这种智能体通过结合大语言模型与关键模块&#xff0c;如规划和记忆&#xff0c;来执行任务。构建这类智能体时&#xff0c;LLM充当着控制中心或“大脑”的角色&#xff0…

浏览器设置字体1px时,不能正常显示

问题如下&#xff0c;某版本浏览器&#xff0c;字体很小时&#xff0c;无法正常显示&#xff0c;解决方案有三种。 解决方案1 解决方案2 解决方案3

WebServer -- 面试题(下)

&#x1f442; 夏风 - Gifty - 单曲 - 网易云音乐 目录 &#x1f33c;前言 &#x1f382;面试题(下) 4&#xff09;HTTP报文解析 为什么要用状态机 状态转移图画一下 https 协议为什么安全 https 的 ssl 连接过程 GET 和 POST 的区别 5&#xff09;数据库注册登录 登…

十五、软考-系统架构设计师笔记-面向服务架构设计理论与实践

1、SOA相关概念 面向服务的架构(SOA)的定义 SOA 是一个组件模型&#xff0c;它将应用程序的不同功能单元(称为服务)通过这些服务之间定义良好的接口和契约联系起来。接口是采用中立的方式进行定义的&#xff0c;它应该独立于实现服务的硬件平台、操作系统和编程语言。这使得构…

[RoarCTF 2019]Easy Java -不会编程的崽

考察一下大家对java-web知识点的掌握 熟悉的登录界面让你想起了某位故人没有&#xff0c;哈哈&#xff0c;但是并非sql注入。一番基础尝试无果后&#xff0c;看看help吧 这个url让你想起某位故人了吗&#xff1f;对文件下载。但是似乎没有响应。改成post请求即可。 我看见pk了&…

unity3d Animal Controller的Animal组件中General基础部分理解

控制器介绍 动物脚本负责控制动物的所有运动逻辑.它管理所有的动画师和刚体参数,以及所有的状态和模式,动物可以做。 动物控制器 是一个动画框架控制器,根动或到位,为任何生物或人形。它利用刚体与物理世界的互动和动画师的玩动画。 States States 是不互相重叠的动画。例如…

canvas绘制圆形进度条

canvas绘制圆形进度条 用canvas绘制了一个圆形进度条&#xff0c;记录一下&#xff1a; 效果如下&#xff1a; 感觉效果还行&#xff0c;不过有待优化 代码如下&#xff1a; 我是用Vue写的 组件的代码&#xff1a; progressCanvas.vue <template><div><can…

Linux VM虚拟环境 设置静态IP

目录 查看自己的网卡配置说明重启网卡实例测试配置情况测试网络 查看自己的网卡 ip a配置文件 vim /etc/sysconfig/network-scripts/ifcfg-ens32配置说明 [rootlinux-server ~]# cd /etc/sysconfig/network-scripts/ #网卡配置文件存放路径 [rootlinux-server network-scri…

C/C++中{}的用法总结(全)

C基础专栏&#xff1a;http://t.csdnimg.cn/UjhPR 目录 1.定义初始化列表&#xff08;Initializer List&#xff09; 2.类成员初始化列表 3.无默认构造函数的类的默认初始化&#xff08;C11 及以后版本&#xff09; 4.初始化器列表构造函数&#xff08;C11 及以后版本&…

js判断对象是否有某个属性

前端判断后端接口是否返回某个字段的时候 <script>var obj { name: "John", age: 30 };console.log(obj.hasOwnProperty("name")); // 输出 trueconsole.log(obj.hasOwnProperty("email")); // 输出 falselet obj11 { name: "Joh…

一文搞懂多模态:BeiT-3之前的14个多模态+4个周边原理解读

在人工智能的世界里&#xff0c;多模态学习不断地展现出其重要性。这个领域的迅速发展不仅促进了不同类型数据之间的深度融合&#xff0c;还为机器理解世界提供了更加丰富和细腻的视角。随着科技的不断演进&#xff0c;人工智能模型已经开始渐渐具备处理和理解从文本、图像&…

MySQL主从复制的原理

MySQL 主从复制是一种数据复制技术&#xff0c;用于在多个数据库服务器之间的数据同步。在主从复制架构中&#xff0c;一个服务器被设置为主服务器&#xff0c;充当数据源&#xff0c;其他服务器被设置为从服务器&#xff0c;用来复制主服务器的数据。 1.主从复制优点 主从复…

Unity Timeline学习笔记(3) - SignalTrack信号轨道和自定义带参数的Marker信号和轨道

信号轨道&#xff0c;顾名思义就是运行到某处发送一个信号。 普通用法 普通用法就是没有任何封装的&#xff0c;个人感觉特别难用&#xff0c;但是有必要理解一下工作原理。 添加信号 我们添加一个信号资源 生成后可以看到资源文件&#xff0c;这个是可以拖到SignalTrack上…

web 服务搭建

实验拓扑图&#xff1a; 实验环境搭建 Centos7-5作为Client&#xff08;12.0.0.12/24&#xff09;&#xff1b;Centos7-1作为网关服务器&#xff08;配置两块网卡ens33 192.168.246.7/24&#xff0c;ens36 12.0.0.1/24&#xff09;&#xff1b;Centos7-2作为Web1&#xff08;19…

2024年云仓酒庄深惠公司发布会圆满落幕:共襄盛举,携手共赢

原标题&#xff1a;2024年云仓酒庄深惠公司发布会圆满落幕&#xff1a;共襄盛举&#xff0c;携手共赢 随着岁月的流转&#xff0c;云仓酒庄以其卓越的品质和不懈的创新精神&#xff0c;在葡萄酒行业中独树一帜。2024年&#xff0c;云仓酒庄再次迎来了一场盛大的发布会——深惠…