Spring StateMachine概念及应用

Spring StateMachine是Spring官方提供的一个框架,供应用程序开发人员在Spring应用程序中使用状态机。支持状态的嵌套(substate)、状态的并行(parallel,fork,join)、子状态机等等。状态机可以帮助开发者简化状态控制的开发过程,使状态机结构更加层次化。

Spring StateMachine概念介绍

Spring StateMachine项目模块

spring state machine spring state machine 原理_状态机


官网地址:https://projects.spring.io/spring-statemachine/

StateMachine关键概念:

  • 状态机(state machine)是一种行为,它指定对象在其生命周期内响应事件所经历的状态序列,以及对象对这些事件的响应。
  • 状态(state)是对象生命周期中满足某种条件、执行某种活动或等待某种事件的条件或情况。
  • 事件(event)在状态机的上下文中,事件是可以触发状态转换的过程。
  • 监视条件(guard condition)在转换的触发事件发生后被评估。只要监视条件不重叠,就可以从相同的源状态中和相同的事件触发器中进行多个转换。在事件发生时,一个保护条件只为转换评估一次。可以用布尔表达式来进行控制。
  • 转换(transition)是两种状态之间的关系,表示处于第一种状态的对象将执行某些操作,并在指定的事件发生且满足指定的条件时进入第二种状态。活动是状态机中正在进行的非原子执行。
  • 动作(action)是一种可执行的原子计算,它导致模型状态的改变或值的返回(一般发生在子状态或者选择状态之间的转换中)。
  • 状态流程配置(StateConfig)是状态机的核心,状态机中状态流转以及事件的触发都是基于状态的配置。Spring StateMachine除去支持简单的状态配置外还支持choice、join、fork、history等状态类型的配置,Spring StateMachine涵盖几乎所有状态机的的状态类型,我们可以充分利用这点特性来完成业务需求。
  • 转换、事件触发器(EventConfig)是定义状态A流转到到状态B会触发的事件,在Spring StateMachine中可以使用OnTransition注解来进行转换的监听或在定义状态流转时进行定义。

StateMachine要素

状态机可归纳为4个要素,现态、条件、动作、次态。“现态”和“条件”是因,“动作”和“次态”是果。

  • 现态:指当前所处的状态。
  • 条件:又称“事件”,当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。
  • 动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必须的,当条件满足后,也可以不执行任何动作,直接迁移到新的状态。
  • 次态:条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转换成“现态”。

StateMachine使用场景

  • 程序的某些结构或组成部分可以看作为一个流程的不同状态
  • 希望将程序中复杂的逻辑拆分为较小的可管理的任务
  • 需要解决程序中的某些并发问题,如程序的异步执行
  • 需要循环遍历if-else结构,并对其做进一步的异常处理

状态改变流程

初始化状态 >> 触发事件 >> 消息通知 >> 状态改变

对应Spring StateMachine的核心步骤为:

  • 定义状态枚举
  • 定义事件枚举
  • 定义状态机配置,设置初始状态,以及状态与事件之间的关系
  • 定义状态监听器,当状态变更时,触发方法

Spring StateMachine的使用

如果使用 Maven 来构建项目,则需将下面的依赖代码置于 pom.xml 文件中:

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

实例说明

在电商平台中,一个订单会有多种状态:已下单、待支付、已支付、待发货、待收货、已完成等。每一种状态都和变化前的状态以及执行的操作有关。比如当用户点击下单后会生成一个“待支付”的订单;当用户支付完成后这个订单状态就转换为了“待发货”。

前置态+操作 => 后置态

if(前置态1)
{
操作
修改成后置态1
...
}else if(前置态2)
....

spring state machine spring state machine 原理_状态机_02

定义状态枚举和事件枚举

public enum States {
        UNPAID,                 // 待支付
        WAITING_FOR_DELIVER,    // 待发货
        WAITING_FOR_RECEIVE,    // 待收货
        DONE                    // 结束
}
public enum Events {
        PAY,        // 支付
        DELIVER,    // 发货
        RECEIVE     // 收货
}

简单状态机实现

完成状态机的配置,包括:(1)状态机的初始状态和所有状态;(2)状态之间的转移规则

@Configuration
@EnableStateMachine            //启用状态机
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<States, Events> {

    private Logger logger = LoggerFactory.getLogger(getClass());

    //配置初始状态
    @Override
    public void configure(StateMachineStateConfigurer<States, Events> states)
            throws Exception {
        states
                .withStates()
                .initial(States.UNPAID)
                .states(EnumSet.allOf(States.class));
    }

    //配置状态转换的事件关系(多个)
    @Override
    public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
            throws Exception {
        transitions
                .withExternal()
                .source(States.UNPAID).target(States.WAITING_FOR_DELIVER).event(Events.PAY)
                .and()
                .withExternal()
                .source(States.WAITING_FOR_DELIVER).target(States.WAITING_FOR_RECEIVE)
                .event(Events.DELIVER)
                .and()
                .withExternal()
                .source(States.WAITING_FOR_RECEIVE).target(States.DONE).event(Events.RECEIVE);
    }

    @Override
    public void configure(StateMachineConfigurationConfigurer<States, Events> config)
            throws Exception {
        config
                .withConfiguration()
                .listener(listener());
    }

    @Bean
    public StateMachineListener<States, Events> listener() {
        return new StateMachineListenerAdapter<States, Events>() {

            @Override
            public void transition(Transition<States, Events> transition) {
                if(transition.getTarget().getId() == States.UNPAID) {
                    logger.info("订单创建,待支付");
                    return;
                }

                if(transition.getSource().getId() == States.UNPAID
                        && transition.getTarget().getId() == States.WAITING_FOR_DELIVER) {
                    logger.info("用户完成支付,待发货");
                    return;
                }

                if(transition.getSource().getId() == States.WAITING_FOR_DELIVER
                        && transition.getTarget().getId() == States.WAITING_FOR_RECEIVE) {
                    logger.info("订单已发货,待收货");
                    return;
                }

                if(transition.getSource().getId() == States.WAITING_FOR_RECEIVE
                        && transition.getTarget().getId() == States.DONE) {
                    logger.info("用户已收货,订单完成");
                    return;
                }
            }
        };
    }
}

测试类

使用CommandLineRunner接口,在测试类的run方法中启动状态机、发送不同的事件,通过日志验证状态机的流转过程。

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.statemachine.StateMachine;

import javax.annotation.Resource;

@SpringBootApplication
public class Application implements CommandLineRunner {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    @Override
    public void run(String... args) throws Exception {
        createStateMachine();
    }

    @Resource
    StateMachine<OrderStates, OrderEvents> stateMachine;
    public void createStateMachine(){
        stateMachine.start();
        stateMachine.sendEvent(OrderEvents.PAY);
        stateMachine.sendEvent(OrderEvents.DELIVER);
        stateMachine.sendEvent(OrderEvents.RECEIVE);
    }
}

注解监听器

由于上面的实现只是做了一些输出,而在实际业务场景中会有更为复杂的逻辑,因此还有一种方法是将监听器放到独立的类中定义,并通过注入的方式加载进来。

对于状态监听器,Spring StateMachine还提供了优雅的注解配置实现方式,所有在StateMachineListener接口中定义的事件都能通过注解的方式来进行配置实现。因此,在下面的示例中,我们将监听器放到独立的类中定义,并且使用到了@OnTransition注解配置。它省去了原来事件监听器方法中各种if的判断,从而使代码显得更为简洁,具有更好的可读性。

@Component
@WithStateMachine                       //绑定待监听的状态机
public class OrderEventConfig {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @OnTransition(target = "UNPAID")
    public void create(){
        logger.info("订单创建,待支付!");
    }

    @OnTransition(source = "UNPAID",target = "WAITING_FOR_DELIVER")
    public void pay(){
        logger.info("用户完成支付,待发货!");
    }

    @OnTransition(source = "WAITING_FOR_DELIVER",target = "WAITING_FOR_RECEIVE")
    public void deliver(){
        logger.info("订单已发货,待收货!");
    }

    @OnTransition(source = "WAITING_FOR_RECEIVE",target = "DONE")
    public void receive(){
        logger.info("用户已收货,订单完成!");
    }
}

多个状态机共存

在实际项目中一般都会有多个状态机并发执行,比如订单,同一时刻会有不止一个订单在运行,而每个订单都有自己的订单状态机流程。但是在上面的例子中,当执行到某一个状态时,再次刷新页面,不会有任何日志出现。也就是说,当一个状态流程执行到某个状态,再次执行这个状态,是不会有任何输出的,因为状态机的机制是只有在状态切换的时候才会触发事件(event)。因此如果想要实现多个状态机的并行执行,就需要用到builder。

OrderStateMachineBuilder.java

@Component
public class OrderStateMachineBuilder {
    private final static String MACHINEID = "orderStateMachine";
    public StateMachine<OrderStates, OrderEvents> build(BeanFactory beanFactory) throws Exception {
        StateMachineBuilder.Builder<OrderStates, OrderEvents> builder = StateMachineBuilder.builder();
        Logger logger = LoggerFactory.getLogger(getClass());
        logger.info("构建订单状态机");

        builder.configureConfiguration()
                .withConfiguration()
                .machineId(MACHINEID)
                .beanFactory(beanFactory);

        builder.configureStates()
                .withStates()
                .initial(OrderStates.UNPAID)
                .states(EnumSet.allOf(OrderStates.class));

        builder.configureTransitions()
                .withExternal()
                .source(OrderStates.UNPAID).target(OrderStates.WAITING_FOR_DELIVER)
                .event(OrderEvents.PAY)
                .and()
                .withExternal()
                .source(OrderStates.WAITING_FOR_DELIVER).target(OrderStates.WAITING_FOR_RECEIVE)
                .event(OrderEvents.DELIVER)
                .and()
                .withExternal()
                .source(OrderStates.WAITING_FOR_RECEIVE).target(OrderStates.DONE)
                .event(OrderEvents.RECEIVE);

        return builder.build();
    }
}

其中MACHINEID指向EventConfig,是状态机的配置类和事件实现类的关联。为了能调用到EventConfig,需要在EventConfig中注明状态机的ID。这个id对应的就是OrderStateMachineBuilder 里面的MACHINEID,被builder写到.machineId(MACHINEID)里面。

@Component
@WithStateMachine(id = "orderStateMachine")               //绑定待监听的状态机
public class OrderEventConfig {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @OnTransition(target = "UNPAID")
    public void create(){
        logger.info("订单创建,待支付!");
    }
    ...
}

调用状态机。在调用状态机时候,现在每一次调用都会新建一个状态机并发运行。

@Autowired
private OrderStateMachineBuilder orderStateMachineBuilder;
StateMachine<States,Events> stateMachine = orderStateMachineBuilder.build(beanFactory);
stateMachine.start();
stateMachine.sendEvent(Events.PAY);
stateMachine.sendEvent(Events.DELIVER);
stateMachine.sendEvent(Events.RECEIVE);

多种状态机并存

在实际需求中,服务不同的需求所以往往需要多个状态机,因此一个程序可能有不同种类的状态机。在实际操作中,我们只需用MACHINEID来标识不同的状态机流程就可以在一个程序内创建并使用不同的状态机了。

为此,我们再创建一个新的状态机流程,表单状态机,其状态转换图如下:

spring state machine spring state machine 原理_spring_03

同样为其定义状态枚举和事件枚举

public enum FormStates {
    BLANK_FORM, // 空白表单
    FULL_FORM, // 已填写表单
    CONFIRM_FORM, // 已校验表单
    SUCCESS_FORM // 已提交表单
}
public enum FormEvents {
        WRITE, // 填写
        CONFIRM, // 校验
        SUBMIT // 提交
}

分别创建订单状态机构建器和表单状态构建器。

private final static String MACHINEID = "orderStateMachine";
    public StateMachine<OrderStates, OrderEvents> build(BeanFactory beanFactory) throws Exception {
        StateMachineBuilder.Builder<OrderStates, OrderEvents> builder = StateMachineBuilder.builder();
        Logger logger = LoggerFactory.getLogger(getClass());
        logger.info("构建订单状态机");

        builder.configureConfiguration()
                .withConfiguration()
                .machineId(MACHINEID)
                .beanFactory(beanFactory);

...
    
private final static String MACHINEID = "formStateMachine";
    public StateMachine<FormStates, FormEvents> build(BeanFactory beanFactory) throws Exception {
        StateMachineBuilder.Builder<FormStates, FormEvents> builder = StateMachineBuilder.builder();
        Logger logger = LoggerFactory.getLogger(getClass());
        logger.info("构建表单状态机");

        builder.configureConfiguration()
                .withConfiguration()
                .machineId(MACHINEID)
                .beanFactory(beanFactory);
...

各自声明对应的EventConfig

@WithStateMachine(id = "orderStateMachine")       
public class OrderEventConfig {
...
}
@WithStateMachine(id = "formStateMachine")
public class FormEventConfig {
...
}

在测试类中使用不同的builder就能同时引用不同的状态机,两种状态机就可以互不干扰的各自运行了。

@Autowired
private OrderStateMachineBuilder orderStateMachineBuilder;
@Autowired
private FormStateMachineBuilder formStateMachineBuilder;
StateMachine<OrderStates, OrderEvents> orderStateMachine = orderStateMachineBuilder.build(beanFactory);
...
StateMachine<FormStates, FormEvents> formStateMachine = formStateMachineBuilder.build(beanFactory);
...