Spring状态机

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

一、状态模式

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

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

下面是状态模式的类图

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

  • 上下文(Context)角色:封装了状态的实例,负责维护状态实例,并将请求委托给当前的状态对象。
  • 抽象状态(State)角色:定义了表示不同状态的接口,并封装了该状态下的行为。所有具体状态都实现这个接口。
  • 具体状态(Concrete State)角色:具体实现了抽象状态角色的接口,并封装了该状态下的行为。

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

java 复制代码
/**
 * @description: 抽象状态类
 */
public abstract class MyState {
    abstract void handler();
}
java 复制代码
/**
 * @description: 具体状态A
 */
public class RedLightState extends MyState{

    @Override
    void handler() {
        System.out.println("红灯停");
    }
}
java 复制代码
/**
 * @description: 具体状态B
 */
public class GreenLightState extends MyState{

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

构建一个上下文环境类,主要用于维护当前状态对象,并提供了切换状态的方法,如下所示:

java 复制代码
/**
 * @description: 环境类
 */
public class MyContext {

    private MyState state;

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

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

构建测试状态模式的示例:

java 复制代码
/**
 * @description: 测试状态模式
 */
public class TestStateMode {

    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(); //绿灯行
    }
}

执行上述代码,其输出结果为:

java 复制代码
红灯停
绿灯行

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

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

二、Spring中的状态机的介绍

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

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

  • 当前状态:也就是状态流转的起始状态。
  • 触发事件:引起状态之间流转的一些动作。
  • 响应函数:触发事件到下一个状态之间的规则。
  • 目标状态:状态流转的目标状态。

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

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

如果想了解Spring状态机的实现原理和使用方式以及其提供的功能,可以通过下述两个地址进行查看:

bash 复制代码
官方文档: https://docs.spring.io/spring-statemachine/docs/4.0.0/reference/index.html#statemachine-config-states
源代码: https://github.com/spring-projects/spring-statemachine

三、Spring状态机实现订单状态流转

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

3.1 环境准备

如果要使用Spring状态机,需要引入对应的jar包,注意Spring状态机的依赖版本要和Spring Boot的版本尽可能保持一致,这里我的Spring Boot版本是2.2.1.RELEASE。

html 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.1.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>bc</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>war</packaging>
	<name>bc</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

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

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.0</version>
			<scope>provided</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

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

java 复制代码
package com.example.bc.entity;

import com.example.bc.constant.OrderStatusEnum;
import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * 模拟订单类
 */
@Data
@AllArgsConstructor
public class Order {

    private Long orderId;

    private OrderStatusEnum orderStatus;
}
java 复制代码
package com.example.bc.constant;

/**
 * "订单状态"枚举
 */
public enum OrderStatusEnum {
    // 待支付
    WAIT_PAYMENT,
    // 待发货
    WAIT_DELIVER,
    // 待收货
    WAIT_RECEIVE,
    // 完成
    FINISH;
}
java 复制代码
package com.example.bc.constant;

/**
 * "订单状态转换行为"枚举
 */
public enum OrderStatusChangeEventEnum {
    //支付
    PAYED,
    //发货
    DELIVERY,
    //收货
    RECEIVED;
}

3.2 构造订单状态机 配置

构建一个针对订单状态流转的状态机配置类:

java 复制代码
package com.example.bc.config;

import com.example.bc.constant.OrderStatusChangeEventEnum;
import com.example.bc.constant.OrderStatusEnum;
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
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 编写状态机监听器

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

java 复制代码
package com.example.bc.listener;

import com.example.bc.constant.OrderStatusEnum;
import com.example.bc.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.Message;
import org.springframework.statemachine.annotation.OnTransition;
import org.springframework.statemachine.annotation.WithStateMachine;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

/**
 * 订单状态监听
 */
@Slf4j
@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);
        log.info("支付,状态机反馈信息:" + 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);
        log.info("发货,状态机反馈信息:" + 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);
        log.info("收货,状态机反馈信息:" + message.getHeaders().toString());
        return true;
    }
}

3.4 编写订单服务类

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

java 复制代码
package com.example.bc.service;

import com.example.bc.entity.Order;

import java.util.Map;

public interface OrderService {

    Order create();

    Order pay(long id);

    Order deliver(long id);

    Order receive(long id);

    Map<Long, Order> getOrders();
}
java 复制代码
package com.example.bc.service.impl;

import com.example.bc.service.OrderService;
import com.example.bc.entity.Order;
import com.example.bc.constant.OrderStatusChangeEventEnum;
import com.example.bc.constant.OrderStatusEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;

import org.springframework.statemachine.StateMachine;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

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

    @Resource
    private StateMachine<OrderStatusEnum, OrderStatusChangeEventEnum> orderStateMachine;

    private long id = 1L;

    private Map<Long, Order> orders = new ConcurrentHashMap<>();

    @Override
    public Order create() {
        Order order = new Order(id++,OrderStatusEnum.WAIT_PAYMENT);
        orders.put(order.getOrderId(), order);
        log.info("订单创建成功:{}",order.getOrderId());
        return order;
    }

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

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

    @Override
    public Order receive(long id) {
        Order order = orders.get(id);
        log.info("尝试收货,订单号:{}", id);
        if (!sendEvent(MessageBuilder.withPayload(OrderStatusChangeEventEnum.RECEIVED)
                .setHeader("order", order).build())) {
            log.info("收货失败,状态异常,订单号:{}", 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模拟客户端用户请求,为了便于展示,这里使用一个测试方法完成所有的操作:

java 复制代码
package com.example.bc.controller;

import com.example.bc.entity.Order;
import com.example.bc.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@Slf4j
@RestController
public class OrderController {

    @Resource
    private OrderService orderService;

    @PostMapping("/testOrderStatusChange")
    public String testOrderStatusChange(){
        Order order1 =orderService.create();
        Order order2 = orderService.create();
        orderService.pay(order1.getOrderId());
        orderService.deliver(order1.getOrderId());
        orderService.receive(order1.getOrderId());
        orderService.pay(order2.getOrderId());
        orderService.deliver(order2.getOrderId());
        orderService.receive(order2.getOrderId());
        log.info("全部订单状态:" + orderService.getOrders());
        return "success";
    }
}

下面是对应的执行结果:

java 复制代码
订单创建成功:1
订单创建成功:2
尝试支付,订单号:1
started org.springframework.statemachine.support.DefaultStateMachineExecutor@2219ba2a
started WAIT_DELIVER WAIT_PAYMENT FINISH WAIT_RECEIVE  / WAIT_PAYMENT / uuid=67349068-b199-4012-ba26-fa16fe88808e / id=null
支付,状态机反馈信息:{order=Order(orderId=1, orderStatus=WAIT_DELIVER), id=9fb9e7b3-5fbe-2271-f151-b52bffde127d, timestamp=1705285872875}
尝试发货,订单号:1
发货,状态机反馈信息:{order=Order(orderId=1, orderStatus=WAIT_RECEIVE), id=41db46e0-be96-37ef-86ac-797c600b8b07, timestamp=1705285872887}
尝试收货,订单号:1
收货,状态机反馈信息:{order=Order(orderId=1, orderStatus=FINISH), id=1e1cf80c-0e3c-7528-47d7-31e268b8c71f, timestamp=1705285872888}
stopped org.springframework.statemachine.support.DefaultStateMachineExecutor@2219ba2a
stopped WAIT_DELIVER WAIT_PAYMENT FINISH WAIT_RECEIVE  /  / uuid=67349068-b199-4012-ba26-fa16fe88808e / id=null
尝试支付,订单号:2
started org.springframework.statemachine.support.DefaultStateMachineExecutor@2219ba2a
started WAIT_DELIVER WAIT_PAYMENT FINISH WAIT_RECEIVE  / WAIT_PAYMENT / uuid=67349068-b199-4012-ba26-fa16fe88808e / id=null
支付,状态机反馈信息:{order=Order(orderId=2, orderStatus=WAIT_DELIVER), id=d6623a1d-e0a3-df0a-bd99-19613d7b7562, timestamp=1705285872889}
尝试发货,订单号:2
发货,状态机反馈信息:{order=Order(orderId=2, orderStatus=WAIT_RECEIVE), id=b52fce26-8099-655f-7e2a-0cee734f2239, timestamp=1705285872890}
尝试收货,订单号:2
收货,状态机反馈信息:{order=Order(orderId=2, orderStatus=FINISH), id=1ed449f8-1590-9507-ad95-91ac2633e3cc, timestamp=1705285872890}
stopped org.springframework.statemachine.support.DefaultStateMachineExecutor@2219ba2a
stopped WAIT_DELIVER WAIT_PAYMENT FINISH WAIT_RECEIVE  /  / uuid=67349068-b199-4012-ba26-fa16fe88808e / id=null
 全部订单状态:{1=Order(orderId=1, orderStatus=FINISH), 2=Order(orderId=2, orderStatus=FINISH)}

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

四、思考

针对状态机的特点,我们还可以使用下述这些方法来实现状态机。

4.1 消息队列

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

4.2 定时任务驱动

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

4.3 规则引擎方式

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

相关推荐
尘浮生4 分钟前
Java项目实战II基于微信小程序的电影院买票选座系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
不是二师兄的八戒28 分钟前
本地 PHP 和 Java 开发环境 Docker 化与配置开机自启
java·docker·php
爱编程的小生40 分钟前
Easyexcel(2-文件读取)
java·excel
带多刺的玫瑰1 小时前
Leecode刷题C语言之统计不是特殊数字的数字数量
java·c语言·算法
计算机毕设指导62 小时前
基于 SpringBoot 的作业管理系统【附源码】
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
Gu Gu Study2 小时前
枚举与lambda表达式,枚举实现单例模式为什么是安全的,lambda表达式与函数式接口的小九九~
java·开发语言
Chris _data2 小时前
二叉树oj题解析
java·数据结构
牙牙7052 小时前
Centos7安装Jenkins脚本一键部署
java·servlet·jenkins
paopaokaka_luck2 小时前
[371]基于springboot的高校实习管理系统
java·spring boot·后端
以后不吃煲仔饭2 小时前
Java基础夯实——2.7 线程上下文切换
java·开发语言