状态机引擎实现调研

今天来分享了一下,前段时间状态机引擎实现调研结果

背景

  • 1.很多文章都分享过状态管理的相关方案,方案和具体业务相关性强,可以给我们提供通用的设计思路与方法论,很难提供一些通用工具组件出来
  • 2.本次调研的主要目的是,提供实现状态引擎可以共用的工具组件,来提高状态管理相关的开发效率,避免重复造轮子
  • 3.本次主要调研了业界常用的、开源的、社区文档说明较多的两种方案
  • 4.spring-statemachine是spring官方提供的状态机实现
  • 5.squirrel-foundation是一个开源的轻量级的状态机实现(马蜂窝交易系统、美团ERP研发中心方案使用过该方案)

状态机简介

状态机概念

状态机是一种用来进行对象行为建模的工具,描述对象在生命周期内所经历的状态变化

特点

  • 有限状态机一般都有以下特点:
    可以用状态来描述事物,并且任一时刻,事物总是处于一种状态事物拥有的状态总数是有限的通过触发事物的某些行为,可以导致事物从一种状态过渡到另一种状态事物状态变化是有规则的,A状态可以变换到B,B可以变换到C,A却不一定能变换到C同一种行为,可以将事物从多种状态变成同种状态,但是不能从同种状态变成多种状态。

适用场景

适用与对象有一个明确的生命周期,并且在生命周期的状态变迁中存在不同的触发条件以及达到状态需要执行的动作。将所有的状态、事件、动作都抽离出来,对复杂的状态迁移逻辑进行统一管理,来取代冗长的 if else 判断,明确的状态条件、原子的响应动作、事件驱动迁移目标状态,使系统更加易于维护和管理

状态机要素

状态机可归纳为4个要素:

即现态、事件、动作、次态。“现态”和“条件”是因,“动作”和“次态”是果。详解如下:

现态

现态:是指当前所处的状态

事件

事件:又称为“条件”。当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移

动作

动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态

次态

次态:条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了

技术选型对比

spring statemachine

spring-statemachine是spring官方提供的状态机实现
Star 892
开源地址如下: https://github.com/spring-projects/spring-statemachine
官方文档如下: https://projects.spring.io/spring-statemachine

优点

轻量级,可以快速接入(已经接入spring框架的应用)内置redis等持久化手段可以通过对接zookpeer实现分布式状态管理模型比较复杂支持较为负责多层的状态模型配置支持使用注解,使用方便,支持基于spring aop上的扩展spring推出,持续迭代

缺点

对于一些没有接入spring框架的应用接入比较困难相关使用文档等较少模型较为负责,接入学习成本相对较高监听器、aop等组件模块都使用了spring框架中的模块,在上面做改造较难

squirrel-foundation

squirrel-foundation一个开源产品 Star 1.4k
开源地址如下: https://github.com/hekailiang/squirrel
官方文档如下: http://hekailiang.github.io/squirrel/

优点

squirrel的实现更为轻量,设计思路也比较清晰,文档以及测试用例也很丰富squirrel关注的人比较大,目前github star,1.4k,贡献采坑问题和问题场景也比较丰富不依赖于spring,没有接入spring框架的应用也可以接入使用支持使用注解,支持自定义切点内部实现不依赖于spring,可以在在上面扩展改造

缺点

切点粒度比较粗,要使用细粒度的切点需要自己实现模型相比较spring statemachine较为简单,实现复杂的多层状态模型需要在此基础上开发实现

选择squirrel-foundation

  • 这里选择squirrel-foundation原因如下:
    量级比较轻,扩展和维护相对而言比较容易不强行依赖于spring,没有接入spring框架的应用也可以接入使用,用spring也很方便入点丰富,支持状态进入、状态完成、异常等节点的监听,使转换过程留有足够的切入点支持使用注解定义状态转移,使用方便开源项目关注多,文档与相关资料多

核心数据流

![[状态机核心数据流|状态机核心数据流.png](https://upload-images.jianshu.io/upload_images/6328467-65984904f295f88f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240

State

State:定义出来业务中所有的状态

Event

Event:定义出来造成状态变更的事件

Context

Context:上下文对象,一般存储业务实体等信息(如活动状态机中存储活动对象campaign)

Condition

Condition: 条件,当触发事件时,可以对context对象中的业务实体进行一些条件判断,是否达到设置的要求

Action

Action: 要执行的动作,目前有三类动作,exit从某个状态退出时要执行的动作,transition从一个状态变更到另一个状态要执行的动作,entry进入一个状态要执行的动作

Listener

Listener: 监听器,监听整个状态变更的过程

核心处理流程

借用了网上文档的中图片,觉得这个图描述的比较清楚


状态机核心处理流程.png
状态正常迁移

TransitionBegin--(exit->transition->entry)-->TransitionComplete-->TransitionEnd

状态迁移异常

TransitionBegin--(exit->transition->entry)-->TransitionException-->TransitionEnd

状态迁移事件拒绝

TransitionBegin-->TransitionDeclined-->TransitionEnd

接入demo

pom文件
<dependency>
    <groupId>org.squirrelframework</groupId>
    <artifactId>squirrel-foundation</artifactId>
    <version>0.3.8</version>
</dependency>
定义状态State
public enum LinkStates {

    LINK_INIT_STATE(1,"初始状态"),
    LINK_NEW_STATE(2,"未发布状态"),
    LINK_RUN_STATE(3,"执行种状态"),
    LINK_DELETE_STATE(1,"已删除状态"),
    LINK_PAUSE_STATE(4,"暂停状态"),
    LINK_END_STATE(5,"结束状态");

    private int state;
    private String name;

    public int getState() {
        return state;
    }

    public String getName() {
        return name;
    }

    private LinkStates(int state, String name) {
        this.state = state;
        this.name = name;
    }
}
定义事件Event
public enum LinkStatesEvent {

    LINK_BUILD_EVENT(1,"新建操作","新建操作状态为2",0,2),
    LINK_RELEASE_EVENT(2,"发布操作","发布操作,状态从2变为3",2,3),
    LINK_PAUSE_EVENT(3,"暂停操作","暂停操作,状态从3变为4",3,4),
    LINK_CONTINUE_EVENT(4,"继续操作","继续操作,状态从4变为3",4,3),
    LINK_END1_EVENT(5,"执行中结束操作","结束操作,状态从3变为5",3,5),
    LINK_END2_EVENT(6,"暂停中结束操作","结束操作,状态从4变为5",4,5),
    LINK_DELETE_EVENT(7,"删除操作","删除操作,状态从2变为1",2,1);


    private int opType;
    private String name;
    private String msg;
    private int from;
    private int to;

    private LinkStatesEvent(int opType, String name, String msg, int from, int to) {
        this.opType = opType;
        this.name = name;
        this.msg = msg;
        this.from = from;
        this.to = to;
    }

    public int getOpType() {
        return opType;
    }

    public String getName() {
        return name;
    }

    public String getMsg() {
        return msg;
    }

    public int getFrom() {
        return from;
    }

    public int getTo() {
        return to;
    }
}
定义上下文Context
public class MyContext {

    //这里使用自己业务对象 活动对象
    private CampaignInfo campaignInfo;

    public CampaignInfo getCampaignInfo() {
        return campaignInfo;
    }

    public void setCampaignInfo(CampaignInfo campaignInfo) {
        this.campaignInfo = campaignInfo;
    }
}
定义状态机,要执行的动作可以定义到状态机对象中

基类为: StateMachine<T, S, E, C>需要把定义好的状态、事件、上下文对象传入

public class MyStateMachine extends AbstractStateMachine<MyStateMachine, LinkStates, LinkStatesEvent, MyContext> {


    /**
     * 动作1:transition动作 从 状态 from 变动到 to状态,触发事件并满足条件后执行
     * @param from 现态
     * @param to 次态
     * @param event 事件
     * @param context 上下文对象
     */
    public void transition(LinkStates from, LinkStates to, LinkStatesEvent event, MyContext context) {
        System.out.println("transition() 方法执行了。。。。。。。。。。。。。 from:" + from + ", to:" + to + ", " +
                "event:" + event + ", context:" + context.getCampaignInfo());
    }

    /**
     * 动作1:exit动作 从 状态 from 退出后,触发事件并满足条件后执行
     * @param from 现态
     * @param to 次态
     * @param event 事件
     * @param context 上下文对象
     */
    protected void exitFrom(LinkStates from, LinkStates to, LinkStatesEvent event, MyContext context){
        System.out.println("exitFrom() 方法执行了。。。。。。。。。。。。。 from:" + from + ", to:" + to + ", " +
                "event:" + event + ", context:" + context.getCampaignInfo());
    }

    /**
     * 动作1:entry动作 进入to状态后,触发事件并满足条件后执行
     * @param from 现态
     * @param to 次态
     * @param event 事件
     * @param context 上下文对象
     */
    protected void entryTo(LinkStates from, LinkStates to, LinkStatesEvent event, MyContext context){
        System.out.println("entryTo() 方法执行了。。。。。。。。。。。。。 from:" + from + ", to:" + to + ", " +
                "event:" + event + ", context:" + context.getCampaignInfo());
    }

}
定义监听器
public class MyStateListener  {


    @OnTransitionBegin
    public void transitionBegin(LinkStatesEvent event) {
        System.out.println("转换开始执行.." + event);
    }
    

    @OnTransitionBegin(when = "context.num == 20 || event.getName().equals(\"新建操作\")")
    public void begins(LinkStates from, LinkStates to, LinkStatesEvent event, MyContext context) {
        System.out.println("begins 执行了, from:" + from + ", to:" + to + ", event:" + event + ", context:" + context.getCampaignInfo());
    }

    @OnTransitionEnd
    public void transitionEnd() {
        System.out.println("转换结束执行..");
    }

    @OnTransitionComplete
    public void transitionComplete(LinkStates from, LinkStates to, LinkStatesEvent event, MyContext context) {
        System.out.println("转换成功执行..");
    }

    @OnTransitionDecline
    public void transitionDeclined(LinkStates from, LinkStatesEvent event, MyContext context) {
        System.out.println("转换拒绝执行..");
    }

    @OnBeforeActionExecuted
    public void onBeforeActionExecuted(LinkStates sourceState, LinkStates targetState,
                                       LinkStatesEvent event, MyContext context, int[] mOfN, Action action) {
        System.out.println("状态机内方法动作执行之前..");
    }

    @OnAfterActionExecuted
    public void onAfterActionExecuted(LinkStates sourceState, LinkStates targetState,
                                      LinkStatesEvent event, MyContext context, int[] mOfN, Action action) {
        System.out.println("状态机内方法动作执行之后..");
    }

    @OnActionExecException
    public void onActionExecException(Action action, TransitionException e) {
        System.out.println("转换异常执行..");
    }

}

这里使用了注解定义了监听器

@OnTransitionBegin

转换开始执行

@OnTransitionBegin(when = "context.campaignInfo.state == 1 || event.name.equals("新建操作")")

这里使用了条件判断,判断条件单独说

@OnTransitionEnd

转换结束执行

@OnTransitionComplete

转换成功执行

@OnTransitionDecline

转换拒绝执行

@OnBeforeActionExecuted

状态机内动作执行之前

@OnAfterActionExecuted

状态机内方法动作执行之后

@OnActionExecException

转换异常执行

测试代码,API构建状态机
public class TestState {


    public static void main(String[] args) {
        StateMachineBuilder<MyStateMachine, LinkStates, LinkStatesEvent, MyContext> builder =
                StateMachineBuilderFactory.create(MyStateMachine.class, LinkStates.class, LinkStatesEvent.class, MyContext.class);

      
        //设置现态为 1,初始状态,  次态为2,未发布状态
        builder.externalTransition().from(LinkStates.LINK_INIT_STATE).to(LinkStates.LINK_NEW_STATE)
                //造成转化的事件为    新建操作,状态从1变为2
                .on(LinkStatesEvent.LINK_BUILD_EVENT)
                //设置条件  context条件不为空 并且 getCampaignInfo.getState得结果是1的
                .whenMvel("myCondition:::(context!=null && context.getCampaignInfo().getState() == 1)")
                //设置动作 设置方法名 满足条件后 调用 transition方法
                .callMethod("transition");


        MyStateMachine machine = builder.newStateMachine(LinkStates.LINK_INIT_STATE);
        //设置监听器
        machine.addDeclarativeListener(new MyStateListener());
        //开启
        machine.start();

        System.out.println("当前状态为" + machine.getCurrentState());
        System.out.println("模拟业务操作");

        //设置上下文对象
        MyContext context = new MyContext();
        CampaignInfo campaignInfo = new CampaignInfo();
        campaignInfo.setState(1);
        context.setCampaignInfo(campaignInfo);

        //发送事件
        machine.fire(LinkStatesEvent.LINK_BUILD_EVENT, context);

        System.out.println("现在状态为 " + machine.getCurrentState());

    }
}
测试结果:
测试结果.png
注解定义状态机

也提供了注解的定义方式

@States({
        @State(name="A", entryCallMethod="entryStateA", exitCallMethod="exitStateA"),
        @State(name="B", entryCallMethod="entryStateB", exitCallMethod="exitStateB")
})
@Transitions({
        @Transit(from="A", to="B", on="GoToB", callMethod="stateAToStateBOnGotoB"),
        @Transit(from="A", to="A", on="WithinA", callMethod="stateAToStateAOnWithinA", type= TransitionType.INTERNAL)
})

States 注解,定了进入状态要执行的方法,退出状态要执行的方法Transitions 注解,定义状态变更的事件和要执行的方式注解标记到状态机的实现类上

条件判断表达式说明

注解和api创建方式都会使用到条件判断表达式表达式中可以使用 event、context 两个对象中的属性进行判断,但是要实现属性的getcontext.getCampaignInfo().getState() == 1 || event.getName().equals("新建操作"),使用对象.get方法可以获取到具体的值context.campaignInfo().state() == 1 || event.name.equals("新建操作"),使用属性名也可以获取具体的属性值

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

推荐阅读更多精彩内容