前言介绍
本文主要介绍一下状态机以及相关的一些概念。结合一个简单的订单状态流程,示例怎样在Springboot中集成Spring-statemachine。
有限状态机(Finite-state machine)
有限状态机(英语:finite-state machine,缩写:FSM),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
应用FSM模型可以帮助对象生命周期的状态的顺序以及导致状态变化的事件进行管理。将状态和事件控制从不同的业务Service方法的if else中抽离出来。FSM的应用范围很广,对于有复杂状态流,扩展性要求比较高的场景都可以使用该模型。
下面是状态机模型中的4个要素,即现态、条件、动作、次态。
- 现态:是指当前所处的状态。
- 条件:又称为“事件”。当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。
- 动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。
- 次态:条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了。
状态机中,每个状态有着相应的行为,随着行为的触发来切换状态。其中一种做法是使用二维数组实现状态机机制,其中横坐标表示行为,纵坐标表示状态,具体的数值则表示当前的状态。
我们以登录场景设计一个状态机。
设计一张状态机表。
横轴是动作,纵轴是状态
此时它的二维数组,如下所示
- 此外,我们也可以通过状态模式实现一个状态机,状态模式将每一个状态封装成独立的类,具体行为会随着内部状态而改变。状态模式用类表示状态,这样我们就能通过切换类来方便地改变对象的状态,避免了冗长的条件分支语句,
- 让系统具有更好的灵活性和可扩展性。现在,我们定义一个状态枚举,其中包括未连接、已连接、注册中、已注册 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
@WithStateMachine
public 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接口来触发状态机变化
@RestController
public 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: connect
Switch state from CONNECTED to LOGINING: beginToLogin
Switch state from LOGINING to UNCONNECTED: loginFailure
- 从输出可以看到,虽然send了4个事件,但只有三条输出。原因是最后一个LOGOUT事件发生时,状态机是UNCONNECTED状态,没有与LOGOUT事件关联的状态转移,故不操作。
- 使用spring实现的状态机将类之间的关系全部交由了IOC容器做管理,实现了真正意义上的解耦。果然Spring大法好啊。
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 stateMachine;
private final PersistingStateChangeInterceptor interceptor = new
PersistingStateChangeInterceptor();
private final CompositePersistStateChangeListener listeners = new
CompositePersistStateChangeListener();
/**
* 实例化一个新的持久化状态机Handler
*
* @param stateMachine 状态机实例
*/
public PersistStateMachineHandler(StateMachine
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 event, OrderStatus
state) {
stateMachine.stop();
List> withAllRegions =
stateMachine.getStateMachineAccessor()
.withAllRegions();
for (StateMachineAccess 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 state,
Message message, Transition transition,
StateMachine stateMachine);
}
private class PersistingStateChangeInterceptor extends
StateMachineInterceptorAdapter {
// 状态预处理的拦截器方法
@Override
public void preStateChange(State state,
Message message,
Transition transition,
StateMachine stateMachine) {
listeners.onPersist(state, message, transition, stateMachine);
}
}
private class CompositePersistStateChangeListener extends
AbstractCompositeListener implements
PersistStateChangeListener {
@Override
public void onPersist(State state,
Message message,
Transition transition,
StateMachine stateMachine) {
for (Iterator 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 state,
Message message,
Transition transition,
StateMachine 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
@Configuration
public class OrderPersistHandlerConfig {
@Autowired
private StateMachine 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:
@Component
public class OrderStateService {
private PersistStateMachineHandler handler;
public OrderStateService(PersistStateMachineHandler handler) {
this.handler = handler;
}
@Autowired
private OrderRepo repo;
public String listDbEntries() {
List 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());
}
}