返回 导航

SpringBoot / Cloud

hangge.com

SpringBoot - 状态机框架StateMachine使用详解2(guard、action、choice)

作者:hangge | 2023-01-17 09:10

四、Guard

1,基本介绍

    Guard 是一种特殊类型的状态机动作,它被用于限制转换发生的条件。在转换发生之前,它会检查这个条件是否满足,如果满足了,转换就会发生,否则转换就不会发生。
例如:在一个简单的自动售货机状态机中,有一个"投入硬币"状态和一个"选择商品"状态。转换从"投入硬币"状态到"选择商品"状态的条件可能是"已经投入足够的硬币"。那么我们可以使用 Guard 来限制这个转换只有在这个条件被满足时才发生。

2,使用样例

(1)还是以前文(点击查看)的订单状态机为例演示 Guard 的使用。假设我们要求订单从“待支付”变为“待收货”状态需要满足某个条件(这里为方便演示,只有订单 id 不小于 100 的才满足条件),首先我们定义一个如下 Guard 类:
// 订单检查守卫
public class OrderCheckGuard implements Guard<States, Events> {
  // 检查方法
  @Override
  public boolean evaluate(StateContext<States, Events> context) {
    // 获取消息中的订单对象
    Order order = (Order) context.getMessage().getHeaders().get("order");
    // 这里做个特殊处理,订单id小于100的都属于不通过
    if (order.getId() < 100) {
      System.out.println("检查订单:不通过");
      return false;
    } else {
      System.out.println("检查订单:通过");
      return true;
    }
  }
}

(2)接着在状态转换配置中添加这个 Guard
// 状态机的配置类
@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) //支付事件将触发:待支付状态->待收货状态
              .guard(new OrderCheckGuard()) // 状态转换guard
              .and()
            .withExternal()
              .source(States.WAITING_FOR_RECEIVE).target(States.DONE)
              .event(Events.RECEIVE); // 收货事件将触发:待收货状态->结束状态
  }
}

(3)最后测试一下:
  • 首先我们对一个订单 id 小于 100 的订单进行支付,由于 guard 返回为 false,则状态机当前仍然处于“待支付”状态;
  • 接着将订单号修改成 999 后再次付款,由于 guard 返回为 true,因此状态机从“待支付”状态变为“待收货”状态;
@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(1);

    System.out.println("--- 发送支付事件 ---");
    Message message = MessageBuilder.withPayload(Events.PAY)
            .setHeader("order", order).build(); // 构建消息
    boolean result = stateMachine.sendEvent(message);
    System.out.println("> 事件是否发送成功:" + result + ",当前状态:"
            + stateMachine.getState().getId());

    // 修改订单id
    order.setId(999);
    System.out.println("--- 再次发送支付事件 ---");
    message = MessageBuilder.withPayload(Events.PAY)
            .setHeader("order", order).build(); // 构建消息
    result = stateMachine.sendEvent(message);
    System.out.println("> 事件是否发送成功:" + result + ",当前状态:"
            + stateMachine.getState().getId());

    System.out.println("--- 发送收货事件 ---");
    message = MessageBuilder.withPayload(Events.RECEIVE)
            .setHeader("order", order).build(); // 构建消息
    result = stateMachine.sendEvent(message);
    System.out.println("> 事件是否发送成功:" + result + ",当前状态:"
            + stateMachine.getState().getId());
  }
}

五、Action

1,基本介绍

(1)Action 可以用来在状态机的状态转换过程中实现自定义逻辑,如数据库操作,日志记录等。
(2)注意,如果 Action 执行过程中出现了异常,状态机的状态是不会发生变化的。

2,使用样例

(1)假设我们需要在订单从“待支付”变为“待收货”状态时执行一些业务逻辑,首先定义一个 Action
// 支付Action
public class OrderPayAction implements Action<States, Events> {
  // 执行方法
  @Override
  public void execute(StateContext<States, Events> context) {
    // 获取消息中的订单对象
    Order order = (Order) context.getMessage().getHeaders().get("order");
    System.out.println("正在执行订单(" + order.getId() + ")支付处理业务......");
  }
}

(2)接着在状态转换配置中添加这个 Action
// 状态机的配置类
@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) //支付事件将触发:待支付状态->待收货状态
              .action(new OrderPayAction())
              .and()
            .withExternal()
              .source(States.WAITING_FOR_RECEIVE).target(States.DONE)
              .event(Events.RECEIVE); // 收货事件将触发:待收货状态->结束状态
  }
}

(3)最后测试一下:
@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(100);

    System.out.println("--- 发送支付事件 ---");
    Message message = MessageBuilder.withPayload(Events.PAY)
            .setHeader("order", order).build(); // 构建消息
    boolean result = stateMachine.sendEvent(message);
    System.out.println("> 事件是否发送成功:" + result + ",当前状态:"
            + stateMachine.getState().getId());

    System.out.println("--- 发送收货事件 ---");
    message = MessageBuilder.withPayload(Events.RECEIVE)
            .setHeader("order", order).build(); // 构建消息
    result = stateMachine.sendEvent(message);
    System.out.println("> 事件是否发送成功:" + result + ",当前状态:"
            + stateMachine.getState().getId());
  }
}

3,异常处理 Action 

(1)我们在设置普通的业务处理 Action 的同时还可以设置异常处理 Action。假设我们定义一个如下异常处理 Action
// 异常处理Action
public class ErrorHandlerAction implements Action<States, Events> {
  // 执行方法
  @Override
  public void execute(StateContext<States, Events> context) {
    RuntimeException exception = (RuntimeException) context.getException();
    System.out.println("捕获到异常:" + exception);
    //将发生的异常信息记录在StateMachineContext中,在外部可以根据这个这个值是否存在来判断是否有异常发生。
    context.getStateMachine()
            .getExtendedState().getVariables()
            .put(RuntimeException.class, exception);
  }
}

(2)然后在状态转换配置中添加这个 Action 即可:
// 状态机的配置类
@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) //支付事件将触发:待支付状态->待收货状态
              .action(new OrderPayAction(), new ErrorHandlerAction())
              .and()
            .withExternal()
              .source(States.WAITING_FOR_RECEIVE).target(States.DONE)
              .event(Events.RECEIVE); // 收货事件将触发:待收货状态->结束状态
  }
}

(3)为方便测试,我们对业务 Action 稍作修改,在里面人为抛出一个异常:
// 支付Action
public class OrderPayAction implements Action<States, Events> {
  // 执行方法
  @Override
  public void execute(StateContext<States, Events> context) {
    // 获取消息中的订单对象
    Order order = (Order) context.getMessage().getHeaders().get("order");
    System.out.println("正在执行订单(" + order.getId() + ")支付处理业务......");
    // 抛出一个异常
    throw new RuntimeException("这是一个运行错误!");
  }
}

(4)最后运行结果如下:

六、复杂状态机

1,choice 配合 guard 实现分支选择

(1)在实际的业务流程中,状态机不可能像前面的样例一样从开始到结尾只有一条路走到底,而是可能存在多种分支的情况。这时我们可以使用 choice 来做选择,它类似于 javaif 语句,作为条件判断的分支而存在。
状态转换配置中的 withInternal、withExternal 和 withChoice 方法区别:
  • withInternal:此方法用于定义内部转换,即在状态内发生的转换,而不离开该状态。内部转换通常用于处理事件或执行不更改机器状态的操作。
  • withExternal:此方法用于定义外部转换,即在不同状态之间发生的转换。当满足指定的事件或条件时,外部转换会使状态机转换到新状态。
  • withChoice:这种方法用于定义一个选择转换,即在不同的状态之间发生的转换,下一个状态是根据一个称为守卫的函数的结果来选择的。选择转换会在决定进入哪个状态之前检查转换的守卫。

(2)首先我们对订单状态枚举类稍作修改,增加了一个新的状态 WAITING_FOR_CHECK(待检查订单),订单在支付后不会从待支付状态直接到待收货状态,而是先变成待检查状态,检查通过后即进入待收货状态,否则退回到待付款状态。
// 订单状态枚举
public enum States {
  UNPAID,                 // 待支付
  WAITING_FOR_CHECK,      // 待检查订单
  WAITING_FOR_RECEIVE,    // 待收货
  DONE                    // 结束
}

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

(3)然后自定义一个 Guard 守卫用于判断,该类需要实现 Guard 接口,在 evaluate 方法中编写相关的判断逻辑。这里为方便演示,如果订单 id 小于 100 的订单一律返回 false
// 订单检查守卫
public class OrderCheckGuard implements Guard<States, Events> {
  // 检查方法
  @Override
  public boolean evaluate(StateContext<States, Events> context) {
    // 获取消息中的订单对象
    Order order = (Order) context.getMessage().getHeaders().get("order");
    // 这里做个特殊处理,订单id小于100的都属于不通过
    if (order.getId() < 100) {
      System.out.println("检查订单:不通过");
      return false;
    } else {
      System.out.println("检查订单:通过");
      return true;
    }
  }
}

(4)接着在状态机配置类中,使用 withChoice() 和跟随它的 first()last() 结合前面我们定义的 Gaurd 类进行分支判断配置。
这里要特别注意两个地方:
  • 使用 withChoice 时一定要在在初始化上加上 choice,不然的话不生效。
  • 使用 withChoice 时无须设置 event,因为它不需要发送事件来进行触发。只要状态转移到了 source 状态,就会自动触发分支判断。
// 状态机的配置类
@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待支付
            .choice(States.WAITING_FOR_CHECK)  //指定分支状态(多个分支状态则多个choice)
            .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_CHECK)
              .event(Events.PAY) //支付事件将触发:待支付状态->待检查状态
              .and()
            .withChoice()
              .source(States.WAITING_FOR_CHECK)
              .first(States.WAITING_FOR_RECEIVE, new OrderCheckGuard()) //如判断为true ->待收货状态
              .last(States.UNPAID) // 否则 -> 待支付状态
              .and()
            .withExternal()
             .source(States.WAITING_FOR_RECEIVE).target(States.DONE)
             .event(Events.RECEIVE); // 收货事件将触发:待收货状态->结束状态
  }
}

  • 如果有多个分支判断,则可以使用 then 子句,它相当于 else if
.withChoice()
  .source(States.STATE_FIRST)
  .first(States.STATE_SECOND, new MtGuard1())
  .then(States.STATE_THIRD, new MtGuard2())
  .then(States.STATE_FOURTH, new MtGuard3())
  .last(States.STATE_FIFTH)
  .and()

(5)事件监听器的代码如下:
//事件监听器
@Configuration
@WithStateMachine
public class StateMachineEventConfig {

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

  @OnTransition(source = "UNPAID", target = "WAITING_FOR_CHECK")
  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());
  }
}

(6)最后测试一下:
  • 首先我们对一个订单 id 小于 100 的订单进行支付,状态机从“待支付”状态变为“待检查订单”状态,然后自动触发分支判断,由于 guard 返回为 false,则自动又变为“待支付”状态;
  • 接着将订单号修改成 999 后再次付款,状态机从“待支付”状态变为“待检查订单”状态,然后自动触发分支判断,由于 guard 返回为 true,则自动又变为“待收货”状态;
@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(1);

    System.out.println("--- 发送支付事件 ---");
    Message message = MessageBuilder.withPayload(Events.PAY)
            .setHeader("order", order).build(); // 构建消息
    boolean result = stateMachine.sendEvent(message);
    System.out.println("> 事件是否发送成功:" + result + ",当前状态:"
            + stateMachine.getState().getId());

    // 修改订单id
    order.setId(999);
    System.out.println("--- 再次发送支付事件 ---");
    message = MessageBuilder.withPayload(Events.PAY)
            .setHeader("order", order).build(); // 构建消息
    result = stateMachine.sendEvent(message);
    System.out.println("> 事件是否发送成功:" + result + ",当前状态:"
            + stateMachine.getState().getId());

    System.out.println("--- 发送收货事件 ---");
    message = MessageBuilder.withPayload(Events.RECEIVE)
            .setHeader("order", order).build(); // 构建消息
    result = stateMachine.sendEvent(message);
    System.out.println("> 事件是否发送成功:" + result + ",当前状态:"
            + stateMachine.getState().getId());
  }
}

2,增加 action 执行业务逻辑

(1)在choice 判断的时候,从一个状态转变到另外一个状态是不需要事件触发的,因此在监听器中也是无法监听到状态变化。比如我们对上面样例的监听器稍作修改,增加从“待检查订单”到“待收货”,以及从“待检查订单”到“待付款”这两个监听,运行后可以看到者两个方法并不会执行。
//事件监听器
@Configuration
@WithStateMachine
public class StateMachineEventConfig {

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

  @OnTransition(source = "UNPAID", target = "WAITING_FOR_CHECK")
  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());
  }

  // 监听状态从待检查订单到待收货
  @OnTransition(source = "WAITING_FOR_CHECK", target = "WAITING_FOR_RECEIVE")
  public void checkPassed() {
    System.out.println("检查通过,等待收货");
  }

  // 监听状态从待检查订单到待付款
  @OnTransition(source = "WAITING_FOR_CHECK", target = "UNPAID")
  public void checkFailed() {
    System.out.println("检查不通过,等待付款");
  }
}

(2)虽然我们可以将业务代码直接写在 guard 中,但最规范的做法还是使用 Action 来执行业务逻辑。具体做法如下,首先我们定义两个 Action 分别用于在订单检查通过、不通过的情况下执行对应业务:
// 订单检查通过Action
public class OrderCheckPassedAction implements Action<States, Events> {
  // 执行方法
  @Override
  public void execute(StateContext<States, Events> context) {
    // 获取消息中的订单对象
    Order order = (Order) context.getMessage().getHeaders().get("order");
    // 设置新状态
    order.setStates(States.WAITING_FOR_RECEIVE);
    System.out.println("检查通过,执行相关的业务代码......");
  }
}

// 订单检查未通过Action
public class OrderCheckFailedAction implements Action<States, Events> {
  // 执行方法
  @Override
  public void execute(StateContext<States, Events> context) {
    // 获取消息中的订单对象
    Order order = (Order) context.getMessage().getHeaders().get("order");
    // 设置新状态
    order.setStates(States.UNPAID);
    System.out.println("检查未通过,执行相关的业务代码......");
  }
}

(3)然后我们在 withChoice() 分支判断中添加相应的 Action 即可:
// 初始化当前状态机有哪些状态迁移动作
// 有来源状态为source,目标状态为target,触发事件为event
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
        throws Exception {
  transitions
          .withExternal()
            .source(States.UNPAID).target(States.WAITING_FOR_CHECK)
            .event(Events.PAY) //支付事件将触发:待支付状态->待检查状态
            .and()
          .withChoice()
            .source(States.WAITING_FOR_CHECK)
            .first(States.WAITING_FOR_RECEIVE, new OrderCheckGuard(), new OrderCheckPassedAction())
            .last(States.UNPAID, new OrderCheckFailedAction())
            .and()
          .withExternal()
           .source(States.WAITING_FOR_RECEIVE).target(States.DONE)
           .event(Events.RECEIVE); // 收货事件将触发:待收货状态->结束状态
}

3,异常处理 Action

(1)在 choice 中除了可以设置普通的业务处理 Action,同时还可以设置异常处理 Action。假设我们定义一个如下异常处理 Action
// 异常处理Action
public class ErrorHandlerAction implements Action<States, Events> {
  // 执行方法
  @Override
  public void execute(StateContext<States, Events> context) {
    RuntimeException exception = (RuntimeException) context.getException();
    System.out.println("捕获到异常:" + exception);
    //将发生的异常信息记录在StateMachineContext中,在外部可以根据这个这个值是否存在来判断是否有异常发生。
    context.getStateMachine()
            .getExtendedState().getVariables()
            .put(RuntimeException.class, exception);
  }
}

(2)然后在 withChoice() 分支判断中添加该 Action 即可:
transitions
        .withExternal()
          .source(States.UNPAID).target(States.WAITING_FOR_CHECK)
          .event(Events.PAY) //支付事件将触发:待支付状态->待检查状态
          .and()
        .withChoice()
          .source(States.WAITING_FOR_CHECK)
          .first(States.WAITING_FOR_RECEIVE, new OrderCheckGuard(),
                  new OrderCheckPassedAction(), new ErrorHandlerAction())
          .last(States.UNPAID)
          .and()
        .withExternal()
         .source(States.WAITING_FOR_RECEIVE).target(States.DONE)
         .event(Events.RECEIVE); // 收货事件将触发:待收货状态->结束状态
  }
}
(3)为方便测试,我们对业务 Action 稍作修改,在里面人为抛出一个异常:
// 订单检查通过Action
public class OrderCheckPassedAction implements Action<States, Events> {
  // 执行方法
  @Override
  public void execute(StateContext<States, Events> context) {
    // 获取消息中的订单对象
    Order order = (Order) context.getMessage().getHeaders().get("order");
    // 设置新状态
    order.setStates(States.WAITING_FOR_RECEIVE);
    System.out.println("检查通过,执行相关的业务代码......");
    // 抛出一个异常
    throw new RuntimeException("这是一个运行错误!");
  }
}

(4)测试结果如下:
评论

全部评论(0)

回到顶部