前言介绍
本文主要介绍一下状态机以及相关的一些概念。结合一个简单的订单状态流程,示例怎样在 Springboot 中集成 Spring-statemachine。
有限状态机(Finite-state machine)
有限状态机(英语:finite-state machine,缩写:FSM),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
应用 FSM 模型可以帮助对象生命周期的状态的顺序以及导致状态变化的事件进行管理。将状态和事件控制从不同的业务 Service 方法的 if else 中抽离出来。FSM 的应用范围很广,对于有复杂状态流,扩展性要求比较高的场景都可以使用该模型。
下面是状态机模型中的 4 个要素,即现态、条件、动作、次态。
状态机中,每个状态有着相应的行为,随着行为的触发来切换状态。其中一种做法是使用二维数组实现状态机机制,其中横坐标表示行为,纵坐标表示状态,具体的数值则表示当前的状态。
我们以登录场景设计一个状态机。
设计一张状态机表。
横轴是动作,纵轴是状态
此时它的二维数组,如下所示
定义一个环境类,它是实际上是真正拥有状态的对象。
状态模式用类表示状态,这样就能通过切换类来方便地改变对象的状态。我们定义几个状态类。
注意的是,如果某个行为不会触发状态的变化,我们可以抛出一个 RuntimeException 异常。此外,调用时,通过环境类控制状态的切换,如下所示。
Spring StateMachine 让状态机结构更加层次化,可以帮助开发者简化状态机的开发过程。现在,我们来用 Spring StateMachine 进行改造。修改 pom 文件,添加 Maven/gradle 依赖。
dependencies { compile 'org.springframework.statemachine:spring-statemachine-core:1.2.7.RELEASE'}
复制代码
定义一个状态枚举,其中包括未连接、已连接、注册中、已注册 4 种状态。
public enum RegStatusEnum { // 未连接 UNCONNECTED, // 已连接 CONNECTED, // 正在登录 LOGINING, // 登录进系统 LOGIN_INTO_SYSTEM;}
复制代码
定义事件枚举,事件的发生触发状态转换
public enum RegEventEnum { // 连接 CONNECT, // 开始登录 BEGIN_TO_LOGIN, // 登录成功 LOGIN_SUCCESS, // 登录失败 LOGIN_FAILURE, // 注销登录 LOGOUT;}
复制代码
配置状态机,通过注解打开状态机功能。配置类一般要继承 EnumStateMachineConfigurerAdapter 类,并且重写一些 configure 方法以配置状态机的初始状态以及事件与状态转移的联系。
import static com.qyz.dp.state.events.RegEventEnum.BEGIN_TO_LOGIN;import static com.qyz.dp.state.events.RegEventEnum.CONNECT;import static com.qyz.dp.state.events.RegEventEnum.LOGIN_FAILURE;import static com.qyz.dp.state.events.RegEventEnum.LOGIN_SUCCESS;import static com.qyz.dp.state.events.RegEventEnum.LOGOUT;import static com.qyz.dp.state.state.RegStatusEnum.CONNECTED;import static com.qyz.dp.state.state.RegStatusEnum.LOGINING;import static com.qyz.dp.state.state.RegStatusEnum.LOGIN_INTO_SYSTEM;import static com.qyz.dp.state.state.RegStatusEnum.UNCONNECTED;
import java.util.EnumSet;
import org.springframework.context.annotation.Configuration;import org.springframework.statemachine.config.EnableStateMachine;import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import com.qyz.dp.state.events.RegEventEnum;import com.qyz.dp.state.state.RegStatusEnum;
@Configuration@EnableStateMachine // 开启状态机配置public class StateMachineConfig extends EnumStateMachineConfigurerAdapter{
/** * 配置状态机状态 */ @Override public void configure(StateMachineStateConfigurer states) throws Exception { states.withStates() // 初始化状态机状态 .initial(RegStatusEnum.UNCONNECTED) // 指定状态机的所有状态 .states(EnumSet.allOf(RegStatusEnum.class)); }
/** * 配置状态机状态转换 */ @Override public void configure(StateMachineTransitionConfigurer transitions) throws Exception { // 1. connect UNCONNECTED -> CONNECTED transitions.withExternal() .source(UNCONNECTED) .target(CONNECTED) .event(CONNECT) // 2. beginToLogin CONNECTED -> LOGINING .and().withExternal() .source(CONNECTED) .target(LOGINING) .event(BEGIN_TO_LOGIN) // 3. login failure LOGINING -> UNCONNECTED .and().withExternal() .source(LOGINING) .target(UNCONNECTED) .event(LOGIN_FAILURE) // 4. login success LOGINING -> LOGIN_INTO_SYSTEM .and().withExternal() .source(LOGINING) .target(LOGIN_INTO_SYSTEM) .event(LOGIN_SUCCESS) // 5. logout LOGIN_INTO_SYSTEM -> UNCONNECTED .and().withExternal() .source(LOGIN_INTO_SYSTEM) .target(UNCONNECTED) .event(LOGOUT); }}
复制代码
Spring StateMachine 提供了注解配置实现方式,所有 StateMachineListener 接口中定义的事件都能通过注解的方式来进行配置实现。这里以连接事件为案例,@OnTransition 中 source 指定原始状态,target 指定目标状态,当事件触发时将会被监听到从而调用 connect() 方法。
在启动 springboot 时,需要注入状态机的状态,事件的配置。起主要涉及到以下两个类:
StateMachineStateConfigurer < S, E> 配置状态集合以及初始状态,泛型参数 S 代表状态,E 代表事件。
StateMachineTransitionConfigurer 配置状态流的转移,可以定义状态转换接受的事件。
配置事件监听器,事件发生时会触发的操作
import org.springframework.context.annotation.Configuration;import org.springframework.statemachine.annotation.OnTransition;import org.springframework.statemachine.annotation.WithStateMachine;
@Configuration@WithStateMachinepublic class StateMachineEventConfig {
@OnTransition(source = "UNCONNECTED", target = "CONNECTED") public void connect() { System.out.println("Switch state from UNCONNECTED to CONNECTED: connect"); }
@OnTransition(source = "CONNECTED", target = "LOGINING") public void beginToLogin() { System.out.println("Switch state from CONNECTED to LOGINING: beginToLogin"); }
@OnTransition(source = "LOGINING", target = "LOGIN_INTO_SYSTEM") public void loginSuccess() { System.out.println("Switch state from LOGINING to LOGIN_INTO_SYSTEM: loginSuccess"); }
@OnTransition(source = "LOGINING", target = "UNCONNECTED") public void loginFailure() { System.out.println("Switch state from LOGINING to UNCONNECTED: loginFailure"); } @OnTransition(source = "LOGIN_INTO_SYSTEM", target = "UNCONNECTED") public void logout() { System.out.println("Switch state from LOGIN_INTO_SYSTEM to UNCONNECTED: logout"); }}
复制代码
通过注解自动装配一个状态机这里写了一个 rest 接口来触发状态机变化
@RestControllerpublic class WebApi {
@Autowired private StateMachine stateMachine; @GetMapping(value = "/testStateMachine") public void testStateMachine() { stateMachine.start(); stateMachine.sendEvent(RegEventEnum.CONNECT); stateMachine.sendEvent(RegEventEnum.BEGIN_TO_LOGIN); stateMachine.sendEvent(RegEventEnum.LOGIN_FAILURE); stateMachine.sendEvent(RegEventEnum.LOGOUT); }}Switch state from UNCONNECTED to CONNECTED: connectSwitch state from CONNECTED to LOGINING: beginToLoginSwitch state from LOGINING to UNCONNECTED: loginFailure
复制代码
Spring StateMachine 让状态机结构更加层次化,我们来回顾下几个核心步骤:
状态转移的监听器
状态转移过程中,可以通过监听器(Listener)来处理一些持久化或者业务监控等任务。在需要持久化的场景中,可以在状态机模式中的监听器中添加持久化的处理。
其中主要涉及到
StateMachineListener 事件监听器(通过 Spring 的 event 机制实现)。
监听 stateEntered(进入状态)、stateExited(离开状态)、eventNotAccepted(事件无法响应)、transition(转换)、transitionStarted(转换开始)、transitionEnded(转换结束)、stateMachineStarted(状态机启动)、stateMachineStopped(状态机关闭)、stateMachineError(状态机异常)等事件,借助 listener 可以跟踪状态转移。
StateChangeInterceptor 拦截器接口,不同于 Listener。其可以改变状态转移链的变化。主要在 preEvent(事件预处理)、preStateChange(状态变更的前置处理)、postStateChange(状态变更的后置处理)、preTransition(转化的前置处理)、postTransition(转化的后置处理)、stateMachineError(异常处理)等执行点生效。
StateMachine 状态机实例,spring statemachine 支持单例、工厂模式两种方式创建,每个 statemachine 有一个独有的 machineId 用于标识 machine 实例;需要注意的是 statemachine 实例内部存储了当前状态机等上下文相关的属性,因此这个实例不能够被多线程共享。
为了方便扩展更多的 Listener,以及管理 Listeners 和 Interceptors。可以定义一个基于状态机实例的 Handler: PersistStateMachineHandler,以及持久化实体的监听器 OrderPersistStateChangeListener 如下:
监听器的 Handler 以及接口定义 PersistStateMachineHandler:
public class PersistStateMachineHandler extends LifecycleObjectSupport {
private final StateMachine<OrderStatus, OrderStatusChangeEvent> stateMachine; private final PersistingStateChangeInterceptor interceptor = new PersistingStateChangeInterceptor(); private final CompositePersistStateChangeListener listeners = new CompositePersistStateChangeListener();
/** * 实例化一个新的持久化状态机Handler * * @param stateMachine 状态机实例 */ public PersistStateMachineHandler(StateMachine<OrderStatus, OrderStatusChangeEvent> stateMachine) { Assert.notNull(stateMachine, "State machine must be set"); this.stateMachine = stateMachine; }
@Override protected void onInit() throws Exception { stateMachine.getStateMachineAccessor().doWithAllRegions(function -> function.addStateMachineInterceptor(interceptor)); }
/** * 处理entity的事件 * * @param event * @param state * @return 如果事件被接受处理,返回true */ public boolean handleEventWithState(Message<OrderStatusChangeEvent> event, OrderStatus state) { stateMachine.stop(); List<StateMachineAccess<OrderStatus, OrderStatusChangeEvent>> withAllRegions = stateMachine.getStateMachineAccessor() .withAllRegions(); for (StateMachineAccess<OrderStatus, OrderStatusChangeEvent> a : withAllRegions) { a.resetStateMachine(new DefaultStateMachineContext<>(state, null, null, null)); } stateMachine.start(); return stateMachine.sendEvent(event); }
/** * 添加listener * * @param listener the listener */ public void addPersistStateChangeListener(PersistStateChangeListener listener) { listeners.register(listener); }
/** * 可以通过 addPersistStateChangeListener,增加当前Handler的PersistStateChangeListener。 * 在状态变化的持久化触发时,会调用相应的实现了PersistStateChangeListener的Listener实例。 */ public interface PersistStateChangeListener {
/** * 当状态被持久化,调用此方法 * * @param state * @param message * @param transition * @param stateMachine 状态机实例 */ void onPersist(State<OrderStatus, OrderStatusChangeEvent> state, Message<OrderStatusChangeEvent> message, Transition<OrderStatus, OrderStatusChangeEvent> transition, StateMachine<OrderStatus, OrderStatusChangeEvent> stateMachine); }
private class PersistingStateChangeInterceptor extends StateMachineInterceptorAdapter<OrderStatus, OrderStatusChangeEvent> { // 状态预处理的拦截器方法 @Override public void preStateChange(State<OrderStatus, OrderStatusChangeEvent> state, Message<OrderStatusChangeEvent> message, Transition<OrderStatus, OrderStatusChangeEvent> transition, StateMachine<OrderStatus, OrderStatusChangeEvent> stateMachine) { listeners.onPersist(state, message, transition, stateMachine); } }
private class CompositePersistStateChangeListener extends AbstractCompositeListener<PersistStateChangeListener> implements PersistStateChangeListener { @Override public void onPersist(State<OrderStatus, OrderStatusChangeEvent> state, Message<OrderStatusChangeEvent> message, Transition<OrderStatus, OrderStatusChangeEvent> transition, StateMachine<OrderStatus, OrderStatusChangeEvent> stateMachine) { for (Iterator<PersistStateChangeListener> iterator = getListeners().reverse(); iterator.hasNext(); ) { PersistStateChangeListener listener = iterator.next(); listener.onPersist(state, message, transition, stateMachine); } } }
}
复制代码
持久化状态发生变化的订单实体的 Listener 实现类 OrderPersistStateChangeListener:
public class OrderPersistStateChangeListener implements PersistStateMachineHandler.PersistStateChangeListener {
@Autowired private OrderRepo repo;
@Override public void onPersist(State<OrderStatus, OrderStatusChangeEvent> state, Message<OrderStatusChangeEvent> message, Transition<OrderStatus, OrderStatusChangeEvent> transition, StateMachine<OrderStatus, OrderStatusChangeEvent> stateMachine) { if (message != null && message.getHeaders().containsKey("order")) { Integer order = message.getHeaders().get("order", Integer.class); Order o = repo.findByOrderId(order); OrderStatus status = state.getId(); o.setStatus(status); repo.save(o);
} }}
复制代码
Springboot 注入 Handler 和 Listener bean 的 Configuration 类,OrderPersistHandlerConfig
@Configurationpublic class OrderPersistHandlerConfig {
@Autowired private StateMachine<OrderStatus, OrderStatusChangeEvent> stateMachine;
@Bean public OrderStateService persist() { PersistStateMachineHandler handler = persistStateMachineHandler(); handler.addPersistStateChangeListener(persistStateChangeListener()); return new OrderStateService(handler); }
@Bean public PersistStateMachineHandler persistStateMachineHandler() { return new PersistStateMachineHandler(stateMachine); }
@Bean public OrderPersistStateChangeListener persistStateChangeListener(){ return new OrderPersistStateChangeListener(); }}
复制代码
订单服务的 Controller&Service 示例
示例提供了两个简单的接口,一个是查看所有订单列表,一个是改变一个订单的状态。
Controller 如下 OrderController:
@RestController@RequestMapping("/orders")public class OrderController {
@Autowired private OrderStateService orderStateService;
/** * 列出所有的订单列表 * * @return */ @RequestMapping(method = {RequestMethod.GET}) public ResponseEntity orders() { String orders = orderStateService.listDbEntries(); return new ResponseEntity(orders, HttpStatus.OK);
}
/** * 通过触发一个事件,改变一个订单的状态 * @param orderId * @param event * @return */ @RequestMapping(value = "/{orderId}", method = {RequestMethod.POST}) public ResponseEntity processOrderState(@PathVariable("orderId") Integer orderId, @RequestParam("event") OrderStatusChangeEvent event) { Boolean result = orderStateService.change(orderId, event); return new ResponseEntity(result, HttpStatus.OK); }
}
复制代码
订单服务类 OrderStateService:
@Componentpublic class OrderStateService {
private PersistStateMachineHandler handler;
public OrderStateService(PersistStateMachineHandler handler) { this.handler = handler; }
@Autowired private OrderRepo repo;
public String listDbEntries() { List<Order> orders = repo.findAll(); StringJoiner sj = new StringJoiner(","); for (Order order : orders) { sj.add(order.toString()); } return sj.toString(); }
public boolean change(int order, OrderStatusChangeEvent event) { Order o = repo.findByOrderId(order); return handler.handleEventWithState(MessageBuilder.withPayload(event).setHeader("order", order).build(), o.getStatus()); }
}
复制代码
评论