写点什么

🍃【SpringBoot 技术专题】「StateMachine」FSM 状态机设计及实现

发布于: 2021 年 06 月 13 日
🍃【SpringBoot技术专题】「StateMachine」FSM状态机设计及实现

前言介绍

本文主要介绍一下状态机以及相关的一些概念。结合一个简单的订单状态流程,示例怎样在 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@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
复制代码


  • 从输出可以看到,虽然 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<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()); }
}
复制代码


发布于: 2021 年 06 月 13 日阅读数: 367
用户头像

我们始于迷惘,终于更高水平的迷惘。 2020.03.25 加入

🏆 【酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“】 🏅 【Java技术领域,MySQL技术领域,APM全链路追踪技术及微服务、分布式方向的技术体系等】 🤝未来我们希望可以共同进步🤝

评论

发布
暂无评论
🍃【SpringBoot技术专题】「StateMachine」FSM状态机设计及实现