51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Spring State Machine(状态机)入门

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

1、状态模式 {#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(状态机) {#2spring-state-machine状态机}

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

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

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

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

| | Spring 状态机 | COLA 状态机 | |--------|-----------------------------------------|-----------------------------------------------| | API 调用 | 使用 Reactive 的 MonoFlux 方式进行 API 调用 | 同步的 API 调用,如果有需要也可以将方法通过 MQ、定时任务、多线程等方式进行异步调用 | | 代码量 | core 包 284 个接口和类 | 36 个接口和类 | | 生态 | 非常丰富 | 较为贫瘠 | | 定制化难度 | 困难 | 简单 |

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

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

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

3、Spring 状态机实现订单状态流转 {#3spring-状态机实现订单状态流转}

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

3.1、环境准备 {#31环境准备}

首先,如果要使用 spring 状态机,需要引入对应的 jar 包,这里我的 Spring Boot 版本是: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、构造订单状态机 {#32构造订单状态机}

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

订单状态机配置类如下:

/**
 * @description: 订单状态机
 */
@Configuration
@EnableStateMachine
public class OrderStatusMachineConfig extends StateMachineConfigurerAdapter<OrderStatusEnum, OrderStatusChangeEventEnum> {
/**
 * 配置状态
 */
@Override
public void configure(StateMachineStateConfigurer&lt;OrderStatusEnum, OrderStatusChangeEventEnum&gt; states) throws Exception {
    states.withStates()
            .initial(OrderStatusEnum.WAIT_PAYMENT)
            .end(OrderStatusEnum.FINISH)
            .states(EnumSet.allOf(OrderStatusEnum.class));
}

/**
 * 配置状态转换事件关系
 */
@Override
public void configure(StateMachineTransitionConfigurer&lt;OrderStatusEnum, OrderStatusChangeEventEnum&gt; 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、编写状态机监听器 {#33编写状态机监听器}

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

/**
 * @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 = &quot;WAIT_DELIVER&quot;, target = &quot;WAIT_RECEIVE&quot;)
public boolean deliverTransition(Message message) {
    Order order = (Order) message.getHeaders().get(&quot;order&quot;);
    order.setOrderStatus(OrderStatusEnum.WAIT_RECEIVE);
    System.out.println(&quot;发货,状态机反馈信息:&quot; + message.getHeaders().toString());
    return true;
}

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

}

3.4、编写订单服务类 {#34编写订单服务类}

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

/**
 * @description: 订单服务
 */
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private StateMachine&lt;OrderStatusEnum, OrderStatusChangeEventEnum&gt; orderStateMachine;

private long id = 1L;

private Map&lt;Long, Order&gt; 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(&quot;订单创建成功:&quot; + order.toString());
    return order;
}

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

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

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


@Override
public Map&lt;Long, Order&gt; getOrders() {
    return orders;
}

/**
 * 发送状态转换事件
 * @param message
 * @return
 */
private synchronized boolean sendEvent(Message&lt;OrderStatusChangeEventEnum&gt; 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(&quot;order&quot;);
            if (Objects.nonNull(order) &amp;&amp; Objects.equals(order.getOrderStatus(), OrderStatusEnum.FINISH)) {
                orderStateMachine.stop();
            }
        }
    }
    return result;
}

}

3.5、测试 {#35测试}

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

@RestController
public class OrderController {
@Resource
private OrderService orderService;

@RequestMapping(&quot;/testOrderStatusChange&quot;)
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(&quot;全部订单状态:&quot; + orderService.getOrders());
    return &quot;success&quot;;
}

}

下面是对应的执行结果:

状态机执行结果

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

4、思考 {#4思考}

针对状态机的特点,还有其他思路实现一个状态机吗?下面是一些常规思路!

  1. 消息队列方式

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

  2. 定时任务驱动

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

  3. 规则引擎方式

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

5、总结 {#5总结}

本文主要介绍了设计模式中的状态模式,并在此基础上介绍了 Spring 状态机相关的概念,并根据常见的订单流转场景,介绍了 Spring 状态机的使用方式。


Ref:https://my.oschina.net/u/4090830/blog/10322627

赞(6)
未经允许不得转载:工具盒子 » Spring State Machine(状态机)入门