返回 导航

SpringBoot / Cloud

hangge.com

SpringBoot - 状态机框架StateMachine使用详解1(基本用法、监听器、Message)

作者:hangge | 2023-01-13 09:20

一、基本介绍

1,什么是状态机?

(1)状态机(State Machine)是一种软件设计模式,它可以帮助开发人员管理和控制系统中的状态变化。状态机通常用于描述系统的状态流转,并定义了状态之间的转换规则。
    我们说状态机(State Machine)一般指有限状态机(Finite State Machine,简称 FSM)。有限状态机是一种特殊的状态机,通常用于描述有限个状态和有限个转换的系统。它的状态和转换是固定的,也就是说,系统只能处于有限个状态,且只能通过有限个转换来改变状态。
(2)状态机可归纳为 4 个要素,现态、条件、动作、次态。“现态”和“条件”是因,“动作”和“次态”是果。
  • 现态:指当前所处的状态
  • 条件:又称“事件”,当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移
  • 动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必须的,当条件满足后,也可以不执行任何动作,直接迁移到新的状态。
  • 次态:条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转换成“现态”。

2,什么是 Spring StateMachine?

(1)Spring StateMachine 是一个基于 Spring 框架的状态机框架,使用 Spring StateMachine 可以方便地在 Java 应用中实现状态机功能,并可以与 Spring 框架的其他功能结合使用。
(2)Spring StateMachine 提供了一组简单易用的 API,可以方便地定义状态机的状态、事件和转换。可以使用 Spring 的配置文件来定义状态机的结构,也可以使用 Java 代码来定义。
(3)Spring StateMachine 还提供了一些丰富的功能,例如支持使用拦截器对状态机的事件进行拦截,以及支持使用监听器来监听状态机的状态变化事件。

3,使用 Spring StateMachine 的步骤

  • 步骤1:定义状态枚举和事件枚举
  • 步骤2:定义状态机的初始状态和所有状态
  • 步骤3:定义状态之间的转移规则
  • 步骤4:在业务对象中使用状态机,编写响应状态变化的监听器方法

二、基本用法

1,添加依赖

要使用 Spring StateMachine 框架,首先编辑项目的 pom.xml 文件,添加相关依赖:
<!--加入spring statemachine的依赖-->
<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-core</artifactId>
    <version>2.5.0</version>
</dependency>

2,创建订单状态枚举

这里以订货流程来演示状态机的使用。我们创建一个订单状态枚举类,其中共有三个状态(待支付、待收货、结束)。
// 订单状态枚举
public enum States {
  UNPAID,                 // 待支付
  WAITING_FOR_RECEIVE,    // 待收货
  DONE                    // 结束
}

3,创建订单事件枚举

然后创建一个订单事件枚举类,里面包含两个引起状态迁移的事件(支付、收货)。
// 订单事件枚举
public enum Events {
  PAY,        // 支付
  RECEIVE     // 收货
}

4,创建状态机配置

状态机配置类中包含状态配置、状态转换事件关系配置、监听器配置。
注意:监听器仅仅是负责监听而已,它无法对状态转移流程进行控制。也就是说即使监听器内部代码抛出异常,状态仍然会照常发生变化。如果需要通过业务代码控制状态是否转移、转移分支,需要使用后文介绍的 guardactionchoice
// 状态机的配置类
@Configuration
//该注解用来启用Spring StateMachine状态机功能
@EnableStateMachine
public class StateMachineConfig
        extends EnumStateMachineConfigurerAdapter<States, Events> {
  // 初始化当前状态机拥有哪些状态
  @Override
  public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception {
    states.withStates().initial(States.UNPAID) //定义了初始状态为UNPAID待支付
            .states(EnumSet.allOf(States.class)); //指定States中的所有状态作为该状态机的状态定义
  }

  // 初始化当前状态机有哪些状态迁移动作
  // 有来源状态为source,目标状态为target,触发事件为event
  @Override
  public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
          throws Exception {
    transitions
            .withExternal()
              .source(States.UNPAID).target(States.WAITING_FOR_RECEIVE)
              .event(Events.PAY) //支付事件将触发:待支付状态->待收货状态
              .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) {
          System.out.println("订单创建");
          return;
        }

        // 从未支付->待收货状态
        if(transition.getSource().getId() == States.UNPAID
                && transition.getTarget().getId() == States.WAITING_FOR_RECEIVE) {
          System.out.println("用户支付完毕");
          return;
        }

        // 从待收货->完成状态
        if(transition.getSource().getId() == States.WAITING_FOR_RECEIVE
                && transition.getTarget().getId() == States.DONE) {
          System.out.println("用户已收货");
          return;
        }
      }
    };
  }
}

5,整体流程测试

(1)最后测试一下,我们定义了整个流程的处理过程:
  • start() 就是创建这个订单流程,根据之前的定义,该订单会处于待支付状态。
  • 然后通过调用 sendEvent(Events.PAY) 执行支付操作
  • 最后通过掉用 sendEvent(Events.RECEIVE) 来完成收货操作。
@SpringBootApplication
public class TestApplication implements CommandLineRunner {

  public static void main(String[] args) {
    SpringApplication.run(TestApplication.class, args);
  }

  // 状态机对象
  @Autowired
  private StateMachine<States, Events> stateMachine;

  //在run函数中,我们定义了整个流程的处理过程
  @Override
  public void run(String... args) throws Exception {

    System.out.println("--- 开始创建订单流程 ---");
    stateMachine.start();    //start()就是创建这个订单流程,根据之前的定义,该订单会处于待支付状态,
    System.out.println("> 当前状态:" + stateMachine.getState().getId());

    System.out.println("--- 发送支付事件 ---");
    boolean result1 = stateMachine.sendEvent(Events.PAY);
    System.out.println("> 事件是否发送成功:" + result1 + ",当前状态:"
            + stateMachine.getState().getId());

    System.out.println("--- 再次发送支付事件 ---");
    boolean result2 = stateMachine.sendEvent(Events.PAY);
    System.out.println("> 事件是否发送成功:" + result2 + ",当前状态:"
            + stateMachine.getState().getId());

    System.out.println("--- 发送收货事件 ---");
    boolean result3 = stateMachine.sendEvent(Events.RECEIVE);
    System.out.println("> 事件是否发送成功:" + result3 + ",当前状态:"
            + stateMachine.getState().getId());
  }
}

(2)运行结果如下。要注意的是,上面代码我们连续发送两个 PAY 支付事件,结果只有第一次发送成功。因为此时状态已经转移到了待收货状态,不能再次接收支付事件。

三、状态监听器

1,所有事件监听

(1)上面样例中我们仅仅监听了状态转移事件,其实状态监听器可以实现的功能远不止上面我们所述的内容,它还有更多的事件捕获,我们可以通过查看 StateMachineListener 接口来了解它所有的事件定义:
public interface StateMachineListener<S, E> {
  // 在状态机的状态改变时调用。
  void stateChanged(State<S, E> var1, State<S, E> var2);

  // 在状态机进入新状态时调用
  void stateEntered(State<S, E> var1);

  // 状态机离开旧状态时调用
  void stateExited(State<S, E> var1);

  // 在状态机不能接受某个事件时调用
  void eventNotAccepted(Message<E> var1);

  // 在状态机的状态转换时调用
  void transition(Transition<S, E> var1);

  // 在状态机开始进行状态转换时调用
  void transitionStarted(Transition<S, E> var1);

  // 在状态机完成状态转换时调用。
  void transitionEnded(Transition<S, E> var1);

  // 在状态机开始运行时被调用。
  void stateMachineStarted(StateMachine<S, E> var1);

  // 在状态机停止运行时被调用。
  void stateMachineStopped(StateMachine<S, E> var1);

  // 在状态机发生错误时被调用。参数exception是表示错误的异常对象。
  void stateMachineError(StateMachine<S, E> var1, Exception var2);

  // 在状态机的扩展状态的改变时被调用。
  void extendedStateChanged(Object var1, Object var2);

  void stateContext(StateContext<S, E> var1);
}

(2)下面代码我们又增加了状态机开始进行状态转换、和完成状态转换者两个事件监听:
// 状态机的配置类
@Configuration
//该注解用来启用Spring StateMachine状态机功能
@EnableStateMachine
public class StateMachineConfig
        extends EnumStateMachineConfigurerAdapter<States, Events> {
  // 初始化当前状态机拥有哪些状态
  @Override
  public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception {
    states.withStates().initial(States.UNPAID) //定义了初始状态为UNPAID待支付
            .states(EnumSet.allOf(States.class)); //指定States中的所有状态作为该状态机的状态定义
  }

  // 初始化当前状态机有哪些状态迁移动作
  // 有来源状态为source,目标状态为target,触发事件为event
  @Override
  public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
          throws Exception {
    transitions
            .withExternal()
              .source(States.UNPAID).target(States.WAITING_FOR_RECEIVE)
              .event(Events.PAY) //支付事件将触发:待支付状态->待收货状态
              .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) {
          System.out.println("订单创建");
          return;
        }

        // 从未支付->待收货状态
        if(transition.getSource().getId() == States.UNPAID
                && transition.getTarget().getId() == States.WAITING_FOR_RECEIVE) {
          System.out.println("用户支付完毕");
          return;
        }

        // 从待收货->完成状态
        if(transition.getSource().getId() == States.WAITING_FOR_RECEIVE
                && transition.getTarget().getId() == States.DONE) {
          System.out.println("用户已收货");
          return;
        }
      }

      // 在状态机开始进行状态转换时调用
      @Override
      public void transitionStarted(Transition<States, Events> transition) {
        // 从未支付->待收货状态
        if(transition.getSource().getId() == States.UNPAID
                && transition.getTarget().getId() == States.WAITING_FOR_RECEIVE) {
          System.out.println("用户支付(状态转换开始)");
          return;
        }
      }

      // 在状态机进行状态转换结束时调用
      @Override
      public void transitionEnded(Transition<States, Events> transition) {
        // 从未支付->待收货状态
        if(transition.getSource().getId() == States.UNPAID
                && transition.getTarget().getId() == States.WAITING_FOR_RECEIVE) {
          System.out.println("用户支付(状态转换结束)");
          return;
        }
      }
    };
  }
}

2,使用注解方式配置监听器

(1)对于状态监听器,Spring StateMachine 还提供了优雅的注解配置实现方式,所有 StateMachineListener 接口中定义的事件都能通过注解的方式来进行配置实现。比如,我们可以将上面样例中的状态监听器改用如下注解配置:
注意:监听器仅仅是负责监听而已,它无法对状态转移流程进行控制。也就是说即使监听器内部代码抛出异常,状态仍然会照常发生变化。如果需要通过业务代码控制状态是否转移、转移分支,需要使用后文介绍的 guardactionchoice
//事件监听器
@Configuration
@WithStateMachine
public class StateMachineEventConfig {

  @OnTransition(target = "UNPAID")
  public void create() {
    System.out.println("订单创建");
  }

  @OnTransition(source = "UNPAID", target = "WAITING_FOR_RECEIVE")
  public void pay() {
    System.out.println("用户支付完毕");
  }

  @OnTransitionStart(source = "UNPAID", target = "WAITING_FOR_RECEIVE")
  public void payStart() {
    System.out.println("用户支付(状态转换开始)");
  }

  @OnTransitionEnd(source = "UNPAID", target = "WAITING_FOR_RECEIVE")
  public void payEnd() {
    System.out.println("用户支付(状态转换结束)");
  }

  @OnTransition(source = "WAITING_FOR_RECEIVE", target = "DONE")
  public void receive() {
    System.out.println("用户已收货");
  }
}

(2)这样原先状态机的配置类里面的代码就十分简洁了:
// 状态机的配置类
@Configuration
//该注解用来启用Spring StateMachine状态机功能
@EnableStateMachine
public class StateMachineConfig
        extends EnumStateMachineConfigurerAdapter<States, Events> {
  // 初始化当前状态机拥有哪些状态
  @Override
  public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception {
    states.withStates().initial(States.UNPAID) //定义了初始状态为UNPAID待支付
            .states(EnumSet.allOf(States.class)); //指定States中的所有状态作为该状态机的状态定义
  }

  // 初始化当前状态机有哪些状态迁移动作
  // 有来源状态为source,目标状态为target,触发事件为event
  @Override
  public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
          throws Exception {
    transitions
            .withExternal()
              .source(States.UNPAID).target(States.WAITING_FOR_RECEIVE)
              .event(Events.PAY) //支付事件将触发:待支付状态->待收货状态
              .and()
            .withExternal()
             .source(States.WAITING_FOR_RECEIVE).target(States.DONE)
             .event(Events.RECEIVE); // 收货事件将触发:待收货状态->结束状态
  }
}

四、发送 Message 消息

1,发送消息

(1)StatemachinesendEvent() 方法除了可以发送事件外,还可以发送 MessageMessage 除了会传递事件从而触发状态机的转换外,同时还能传递其他信息方便我们进行业务处理。假设我们需要传递订单信息,首先定义一个如下订单类:
// 订单类
public class Order {
  // 订单号
  private int id;

  // 订单状态
  private States states;

  public void setStates(States states) {
    this.states = states;
  }
  public States getStates() {
    return states;
  }
  public void setId(int id) {
    this.id = id;
  }
  public int getId() {
    return id;
  }
  @Override
  public String toString() {
    return "订单号:" + id + ", 订单状态:" + states;
  }
}

(2)然后使用 MessageBuilder 来创建 Message 实例,并设置 Message 的各种属性。最后通过 sendEvent() 方法发送即可:
@SpringBootApplication
public class TestApplication implements CommandLineRunner {

  public static void main(String[] args) {
    SpringApplication.run(TestApplication.class, args);
  }

  // 状态机对象
  @Autowired
  private StateMachine<States, Events> stateMachine;

  //在run函数中,我们定义了整个流程的处理过程
  @Override
  public void run(String... args) throws Exception {
    System.out.println("--- 开始创建订单流程 ---");
    stateMachine.start();    //start()就是创建这个订单流程,根据之前的定义,该订单会处于待支付状态,

    // 创建订单对象
    Order order = new Order();
    order.setStates(States.UNPAID);
    order.setId(123);

    System.out.println("--- 发送支付事件 ---");
    Message message = MessageBuilder.withPayload(Events.PAY)
            .setHeader("order", order).build(); // 构建消息
    stateMachine.sendEvent(message);


    System.out.println("--- 发送收货事件 ---");
    message = MessageBuilder.withPayload(Events.RECEIVE)
            .setHeader("order", order).build(); // 构建消息
    stateMachine.sendEvent(message);
  }
}

2,接收消息

(1)如果是使用注解方式配置的监听器,可以通过如下方式接收发送的消息:
//事件监听器
@Configuration
@WithStateMachine
public class StateMachineEventConfig {

  @OnTransition(target = "UNPAID")
  public void create() {
    System.out.println("订单创建");
  }

  @OnTransition(source = "UNPAID", target = "WAITING_FOR_RECEIVE")
  public void pay(Message<Events> message) {
    // 获取消息中的订单对象
    Order order = (Order) message.getHeaders().get("order");
    // 设置新状态
    order.setStates(States.WAITING_FOR_RECEIVE);
    System.out.println("用户支付完毕,状态机反馈信息:" + message.getHeaders().toString());
  }

  @OnTransition(source = "WAITING_FOR_RECEIVE", target = "DONE")
  public void receive(Message<Events> message) {
    // 获取消息中的订单对象
    Order order = (Order) message.getHeaders().get("order");
    // 设置新状态
    order.setStates(States.DONE);
    System.out.println("用户已收货,状态机反馈信息:" + message.getHeaders().toString());
  }
}

(2)如何是原始方式的话就会稍微麻烦些,我们需要在 StateMachineListener 中使用它的 stateContext 方法来获取 StateContext。然后通过 StateContextgetMessageHeaders 方法来访问消息头。
// 状态机的配置类
@Configuration
//该注解用来启用Spring StateMachine状态机功能
@EnableStateMachine
public class StateMachineConfig
        extends EnumStateMachineConfigurerAdapter<States, Events> {
  // 初始化当前状态机拥有哪些状态
  @Override
  public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception {
    states.withStates().initial(States.UNPAID) //定义了初始状态为UNPAID待支付
            .states(EnumSet.allOf(States.class)); //指定States中的所有状态作为该状态机的状态定义
  }

  // 初始化当前状态机有哪些状态迁移动作
  // 有来源状态为source,目标状态为target,触发事件为event
  @Override
  public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
          throws Exception {
    transitions
            .withExternal()
              .source(States.UNPAID).target(States.WAITING_FOR_RECEIVE)
              .event(Events.PAY) //支付事件将触发:待支付状态->待收货状态
              .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>() {

      // 保存当前的状态上下文
      private StateContext stateContext;

      // 上下文状态改变时调用
      @Override
      public void stateContext(StateContext<States, Events> stateContext) {
        this.stateContext = stateContext;
      }

      // 在状态机的状态转换时调用
      @Override
      public void transition(Transition<States, Events> transition) {
        // 当前是未支付状态
        if(transition.getTarget().getId() == States.UNPAID) {
          System.out.println("订单创建");
          return;
        }

        // 从未支付->待收货状态
        if(transition.getSource().getId() == States.UNPAID
                && transition.getTarget().getId() == States.WAITING_FOR_RECEIVE) {
          // 获取触发转换的消息
          Message<Events> message = this.stateContext.getMessage();
          // 获取消息中的订单对象
          Order order = (Order) message.getHeaders().get("order");
          // 设置新状态
          order.setStates(States.WAITING_FOR_RECEIVE);
          System.out.println("用户支付完毕,状态机反馈信息:" + message.getHeaders().toString());
          return;
        }

        // 从待收货->完成状态
        if(transition.getSource().getId() == States.WAITING_FOR_RECEIVE
                && transition.getTarget().getId() == States.DONE) {
          // 获取触发转换的消息
          Message<Events> message = this.stateContext.getMessage();
          // 获取消息中的订单对象
          Order order = (Order) message.getHeaders().get("order");
          // 设置新状态
          order.setStates(States.DONE);
          System.out.println("用户已收货,状态机反馈信息:" + message.getHeaders().toString());
          return;
        }
      }
    };
  }
}
评论

全部评论(0)

回到顶部