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)