SpringBoot技术专题-StateMachine状态机设计

前言介绍

本文主要介绍一下状态机以及相关的一些概念。结合一个简单的订单状态流程,示例怎样在Springboot中集成Spring-statemachine

有限状态机(Finite-state machine)

有限状态机(英语:finite-state machine,缩写:FSM),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型

应用FSM模型可以帮助对象生命周期的状态的顺序以及导致状态变化的事件进行管理将状态和事件控制从不同的业务Service方法的if else中抽离出来。FSM的应用范围很广,对于有复杂状态流,扩展性要求比较高的场景都可以使用该模型。

下面是状态机模型中的4个要素,即现态、条件、动作、次态。

  • 现态:是指当前所处的状态。
  • 条件:又称为“事件”。当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。
  • 动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态
  • 次态:条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了

状态机中,每个状态有着相应的行为,随着行为的触发来切换状态。其中一种做法是使用二维数组实现状态机机制,其中横坐标表示行为,纵坐标表示状态,具体的数值则表示当前的状态。

我们以登录场景设计一个状态机。


image.png

设计一张状态机表。

横轴是动作,纵轴是状态

状态机表

此时它的二维数组,如下所示

二维数组

  • 此外,我们也可以通过状态模式实现一个状态机,状态模式将每一个状态封装成独立的类,具体行为会随着内部状态而改变。状态模式用类表示状态,这样我们就能通过切换类来方便地改变对象的状态,避免了冗长的条件分支语句,
  • 让系统具有更好的灵活性和可扩展性。现在,我们定义一个状态枚举,其中包括未连接、已连接、注册中、已注册 4 种状态。
枚举状态类

定义一个环境类,它是实际上是真正拥有状态的对象。


环境上下文类

状态模式用类表示状态,这样就能通过切换类来方便地改变对象的状态。我们定义几个状态类。


状态操作接口
未连接状态的接口
连接状态的接口
注册状态的接口

注意的是,如果某个行为不会触发状态的变化,我们可以抛出一个 RuntimeException 异常。此外,调用时,通过环境类控制状态的切换,如下所示。

image.png

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<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

@Configuration
public 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:

@Component
public 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());
    }

}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容