一、背景
在开发中总会遇到这样的场景,比如工单状态,流程状态,通过状态判断该执行的操作,不断改动的需求导致永无止境的 IF、ELSE 和 BREAK 子句的层次结构,当事情开始看起来太复杂时,简直就像面满池子的海洋球。
办公审批流程相关的可以使用工作流框架解决此类问题。轻量级的稍微复杂些的状态控制可考虑使用 状态机 来帮助梳理结构,把状态的控制和流转集中管理。
状态机之所以强大,是因为它们的行为始终保证一致,并且由于机器启动时操作规则是一成不变的,因此相对容易调试。这个想法是您的应用程序现在处于并且可能存在于有限数量的状态中。然后会发生一些事情,使您的应用程序从一种状态进入另一种状态
二、使用场景
当遇到下列情形时,您可能已在实现了一个状态机:
- 使用布尔标志或枚举来模拟不同的情形。
- 使用变量表示应用程序生命周期的阶段或者状态。
- 循环通过if-else结构 ,检查是否设置了特定的标志或枚举,然后进一步 说明标志和枚举存在或不存在时该怎么做。
Spring Statemachine 是 spring 套件的中一个状态机框架,使用JDK 和SpringFramework 构建。它不需要其核心系统中Spring Framework之外的任何其他依赖项.
三、使用 Spring Statemachine
添加 Maven 依赖
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>3.2.0</version>
</dependency>
实际项目中可能会有多种状态机,通过 MACHIND_ID 作为区分,我们使用builder方式来创建状态机。
如下图所示:事件可看成一种导致状态变更的行为事件。
状态: 待支付 ---------- 待发货 ---------- 待收货 ---------- 结束
\ / \ / \ /
事件: 支付 发货 收货
第一步:定义 状态 和 事件
public enum OrderStates {
UNPAID, // 待支付
WAITING_FOR_DELIVER, // 待发货
WAITING_FOR_RECEIVE, // 待收货
DONE // 结束
}
public enum OrderEvents {
PAY, // 支付
DELIVER, // 发货
RECEIVE // 收货
}
第二步:启用 状态机服务
// 使用注解 EnableStateMachine 启用 状态机服务
@EnableStateMachine
@Configuration
public class StateMachineConfig {
}
第三步:定义一个 状态机的 builder
定义一个 状态机的 builder, 这里定了一个常量 MACHINEID ,它用来区分不同种类的状态机。使用状态机的时候要用到这个 MACHINEID。
@Slf4j
@Configuration
public class OrderStateMachineBuilder {
public 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)
// 加了 action 和捕获异常的 action
.action(action(),errorAction())
.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();
}
@Bean
public Action<OrderStates, OrderEvents> action() {
return new Action<OrderStates, OrderEvents>() {
@Override
public void execute(StateContext<OrderStates, OrderEvents> context) {
// do something
log.info("action 行为: {}",context.getMessage());
// 可以模拟抛出异常,将会触发 errorAction
//throw new RuntimeException("xxx 异常");
}
};
}
@Bean
public Action<OrderStates, OrderEvents> errorAction() {
return new Action<OrderStates, OrderEvents>() {
@Override
public void execute(StateContext<OrderStates, OrderEvents> context) {
// RuntimeException("MyError") added to context
log.info("action 行为-捕获异常: {}",context.getMessage());
Exception exception = context.getException();
exception.getMessage();
}
};
}
}
定义一个监听器,当产生了 状态转换时,触发监听事件。注意 这里用到 MACHINEID ,它表示这个监听的是 指定MACHINEID的状态机。
@Component
// 这里指定了 MACHINEID
@WithStateMachine(id = OrderStateMachineBuilder.MACHINEID) //绑定待监听的状态机
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("用户已收货,订单完成!");
}
}
第四步:使用状态机
@Autowired
private OrderStateMachineBuilder orderStateMachineBuilder;
@Autowired
private BeanFactory beanFactory;
@Override
public void run(String... args) throws Exception {
log.info("# 工作流开始");
StateMachine<OrderStates,OrderEvents> stateMachine = orderStateMachineBuilder.build(beanFactory);
stateMachine.start();
// 触发 支付,发货,收货 等事件
stateMachine.sendEvent(OrderEvents.PAY);
stateMachine.sendEvent(OrderEvents.DELIVER);
stateMachine.sendEvent(OrderEvents.RECEIVE);
}
至此一个 状态机就完成了。
四、扩展
4.1、分支
使用 withChoice 表达分支,它 指定 first ,then, last 这样,等同于 fi elseif else 结构。下面这个示例 的 guardCheckDays() 是一个 guard,用来判断是否为真。类似 if( true/flase ) 判断是否成立已觉得走向,last 就是最后一个 else ,就无须判断条件了。
// 一个选择,判断天数
.withChoice()
.source(LeaveStates.CHECK_DAYS)
.first(LeaveStates.WAIT_FOR_MANAGER_APPROVAL, guardCheckDays())
.last(LeaveStates.DONE)
贴上相关的 guardCheckDays() 方法。
private Guard<LeaveStates, LeaveEvents> guardCheckDays() {
return new Guard<LeaveStates, LeaveEvents>() {
@Override
public boolean evaluate(StateContext<LeaveStates, LeaveEvents> context) {
LeaveEvents event = context.getEvent();
Object result = context.getMessage().getHeaders().get("DAYS");
int days = result == null ? 0 : Integer.parseInt(result.toString());
log.info("# 判断:天数{}, 结果{}", days, days >= 3);
return days >= 3;
}
};
}
4.2 、一些词汇
词汇表
状态机 State Machine
驱动状态集合的主要实体,以及区域、转换和事件。
状态 State
状态模拟某种不变条件成立的情况。状态是状态机的主要实体,其中状态更改由事件驱动。
扩展状态 Extended State
扩展状态是一组特殊的变量,保存在状态机中以减少所需状态的数量。
过渡 Transition
转换是源状态和目标状态之间的关系。它可能是复合转换的一部分,它将状态机从一种状态配置转换为另一种状态配置,表示状态机对特定类型事件发生的完整响应。
事件 Event
发送到状态机然后驱动各种状态更改的实体。
初始状态 Initial State
状态机启动的特殊状态。初始状态总是绑定到特定的状态机或区域。具有多个区域的状态机可能有多个初始状态。
结束状态 End State
(也称为最终状态。)一种特殊的状态,表示封闭区域已完成。如果封闭区域直接包含在状态机中,并且状态机中的所有其他区域也已完成,则整个状态机已完成。
历史状态 History State
一种让状态机记住其最后活动状态的伪状态。存在两种类型的历史状态:浅层(仅记住顶级状态)和深层(记住子机器中的活动状态)。
选择状态 Choice State
允许根据(例如)事件标头或扩展状态变量做出转换选择的伪状态。
结状态 Junction State
一种伪状态,与选择状态比较相似,但允许多个传入转换,而选择只允许一个传入转换。
分叉状态 Fork State
一种伪状态,可以控制进入某个区域。
加入状态 Join State
一种伪状态,可控制区域退出。
入口点 Entry Point
允许受控进入子机的伪状态。
出口点 Exit Point
允许从子机受控退出的伪状态。
地区 Region
区域是复合状态或状态机的正交部分。它包含状态和转换。
警卫 Guard
根据扩展状态变量和事件参数的值动态评估的布尔表达式。保护条件影响状态机的行为,方法是仅在它们评估为时启用操作或转换TRUE,并在评估为时禁用它们FALSE。
行动 Action
动作是在触发转换期间运行的行为。
4.3 、什么也不做的持久化
状态机可以持久到 内存,redis 等。然而实际开发中我们可能不像要这样的持久化,比如我们数据库里的 订单表 可能有个字段status ,我们会不断的修改这个状态,又不想和其他地方产生可能的冲突,那可以考虑使用一个 “什么也不做的持久化”。思路是:
- 每次使用时从数据库下 得到 status 状态
- 使用这个状态,恢复一个新的状态机
- 发送事件,导致一个新状态
- 将新状态再保存到数据库
- 以后每次都这么干
好处是:
1、将 业务逻辑 和 状态机(工作流逻辑)分开了
2、在微服务下(多实例时)也可以使用,而不用考虑 状态机持久化的问题。
@Component
public class OrderStateMachinePersist implements StateMachinePersist<OrderStates, OrderEvents, Order> {
private String machineId = "orderMachine";
@Override
public void write(StateMachineContext<OrderStates, OrderEvents> context, Order contextObj) throws Exception {
//这里不做任何持久化工作
}
@Override
public StateMachineContext<OrderStates, OrderEvents> read(Order contextObj) throws Exception {
StateMachineContext<OrderStates, OrderEvents> result = new DefaultStateMachineContext<OrderStates, OrderEvents>(OrderStates.valueOf(contextObj.getState()),
null, null, null, null, machineId);
return result;
}
}
/* ------- 分割线 ------- */
@Configuration
public class PersistConfig {
@Autowired
private OrderStateMachinePersist orderStateMachinePersist;
@Bean(name="orderPersister")
public StateMachinePersister<OrderStates, OrderEvents, Order> orderPersister() {
return new DefaultStateMachinePersister<OrderStates, OrderEvents, Order>(orderStateMachinePersist);
}
}
/* 在controler 或者 service 中使用时 */
@RequestMapping("/testOrderRestore")
public void testOrderRestore(String id) throws Exception {
StateMachine<OrderStates, OrderEvents> stateMachine = orderStateMachineBuilder.build(beanFactory);
//订单
Order order = new Order();
order.setId(id);
order.setState(OrderStates.WAITING_FOR_RECEIVE.toString());
//恢复
persister.restore(stateMachine, order);
//查看恢复后状态机的状态
System.out.println("恢复后的状态:" + stateMachine.getState().getId());
}
五、参考
Spring StateMachine 官方
https://docs.spring.io/spring-statemachine/docs/3.2.0/reference/