SpringBoot - 状态机框架StateMachine使用详解2(guard、action、choice)
作者:hangge | 2023-01-17 09:10
四、Guard
1,基本介绍
Guard 是一种特殊类型的状态机动作,它被用于限制转换发生的条件。在转换发生之前,它会检查这个条件是否满足,如果满足了,转换就会发生,否则转换就不会发生。
(2)接着在状态转换配置中添加这个 Guard:
(3)最后测试一下:
(2)接着在状态转换配置中添加这个 Action:
(3)最后测试一下:
例如:在一个简单的自动售货机状态机中,有一个"投入硬币"状态和一个"选择商品"状态。转换从"投入硬币"状态到"选择商品"状态的条件可能是"已经投入足够的硬币"。那么我们可以使用 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:
(2)然后在状态转换配置中添加这个 Action 即可:
(3)为方便测试,我们对业务 Action 稍作修改,在里面人为抛出一个异常:
(4)最后运行结果如下:
// 异常处理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 来做选择,它类似于 java 的 if 语句,作为条件判断的分支而存在。
状态转换配置中的 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)