今天来分享了一下,前段时间状态机引擎实现调研结果
背景
- 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: 监听器,监听整个状态变更的过程
核心处理流程
借用了网上文档的中图片,觉得这个图描述的比较清楚
状态正常迁移
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());
}
}
测试结果:
注解定义状态机
也提供了注解的定义方式
@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("新建操作"),使用属性名也可以获取具体的属性值