技术选型比较
框架 | 功能 | 文档 | 维护成本 |
---|---|---|---|
Spring StateMachine | Spring框架、分布式、可配置流程、事件驱动 | 开源、Spring官方文档、文档完善 | 轻量级框架、上手快、维护成本低 |
Activiti | 流程定义、工作流、服务协作 | 开源、文档完善 | 架构重、维护学习成本较大 |
从上对比可以看出,Spring StateMachine框架轻量化、并且流程可配置、支持分布式、维护成本低,故此我们考虑采用此框架来对我们的系统进行重构。
简介
Spring Statemachine 是在Spring框架中应用状态机概念的一套开发框架,支持状态机包含的一系列特性:现态、次态、动作、条件、触发器、状态转移器、事件监听器、异常处理机制,并且支持配置结构复杂的状态机。
状态机介绍
通常我们在设计状态机流程或者是系统时都要提前将所涉及到的状态流转表示出来,内容平台-题库系统选择的是“状态图”,并且采用“Visual Paradigm”软件进行绘制。
状态机关键概念
状态机(state machine)是一种行为,它指定对象在其生命周期内响应事件所经历的状态序列,以及对象对这些事件的响应。
状态(state)是对象生命周期中满足某种条件、执行某种活动或等待某种事件的条件或情况。
事件(event)在状态机的上下文中,事件是可以触发状态转换的过程。
监视条件(guard condition)在转换的触发事件发生后被评估。只要监视条件不重叠,就可以从相同的源状态中和相同的事件触发器中进行多个转换。在事件发生时,一个保护条件只为转换评估一次。可以用布尔表达式来进行控制。
转换(transition)是两种状态之间的关系,表示处于第一种状态的对象将执行某些操作,并在指定的事件发生且满足指定的条件时进入第二种状态。活动是状态机中正在进行的非原子执行。
动作(action)是一种可执行的原子计算,它导致模型状态的改变或值的返回(一般发生在子状态或者选择状态之间的转换中)。
具体的状态图示例参考图1-1(来源自Visual Paradigm官网对状态图的解释:状态图)
图1-1
Spring StateMachine介绍
状态流程配置(StateConfig)
状态配置是状态机的核心,状态机中状态流转以及事件的触发都是基于状态的配置。Spring StateMachine除去支持简单的状态配置外还支持choice、join、fork、history等状态类型的配置,Spring StateMachine涵盖几乎所有状态机的的状态类型,我们可以充分利用这点特性来完成业务需求。
转换、事件触发器(EventConfig)
转换是指状态A流转到到状态B发生的事件,在Spring StateMachine中可以使用OnTransition注解来进行转换的监听。
事件(action)在Spring StateMachine中指的是当发生条件等情况时会触发,这类转换需要在定义状态流转时进行定义,而不是在@OnTransition注解的方法上进行定义。
状态机消息
Spring StateMachine采用Spring原生的Message来进行业务参数的传递,并且在状态流转的各个阶段以及监听器、拦截器中都可以进行获取。
状态机持久化
在实际的业务开发过程中,更多的场景是一个业务状态达到了某一个状态后,后续的状态是否触发以及触发的时间点都是根据用户走的,这样以来每次都从起始点去开始状态机流程就不符合实际业务场景了。Spring StateMachine给出的解决方案是结合redis将状态机存储,使用时通过restore方法去还原状态机到某一个状态节点。
由于题库系统中试卷、试题状态的多样性、复杂性我们选择使用状态机工厂来完成状态机的实现,并且基于Spring StateMachine的部分特性又增加了一些额外的功能来更好的完成工作。
配置统一化
由于所有的状态以及事件配置都是在config中预先配置好的,所以我们可以通过stateMachine获取到所有的配置项,从而做一些校验,这样一来,所有的校验都是基于底层的config,不会出现配置不统一的情况。
/**
* 通过状态获取具体的事件
* @param stateMachine 具体的状态机
* @param source 现状态
* @param target 次状态
*/
public E getEventByStatus(StateMachine<S, E> stateMachine, S source, S target){
for (Transition<S, E> transition : stateMachine.getTransitions()) {
if(transition.getSource().getId().equals(source) && transition.getTarget().getId().equals(target)){
return transition.getTrigger().getEvent();
}
}
return null;
}
以上代码示例中stateMachine.getTransitions()可以获取到当前状态机所有的状态转换配置项
前置处理器
在业务开发中,不可避免的在发生状态转换之前要有一些必要的处理或者是校验,结合“配置统一化”我们抽取了trigger,在trigger中提供根据状态获取事件的方式,并且为每一个事件构造一个前置处理器。
trigger提供了统一的sendEvent方法,并且在发送真正的事件之前会拿到对应的前置处理器并且执行,从而满足大部分业务的需要。每个前置处理器都有自己对应的event bean属性名称,这样在项目启动后,根据对应的event就可以拿到对应的处理器bean。
开启Spring StateMachine的方式有两种:@EnableStateMachine、@EnableStateMachineFactory,我们选择工厂方式@EnableStateMachineFactory,利用UUID创建不同的状态机
第一步-定义状态机状态、事件枚举
/**
* 状态枚举类
*/
public enum States {
S1, S2, S3, S4, S5, S6
}
/**
* 事件枚举类
*/
public enum Events {
E1, E2, E3, E4
}
第二步-状态转移以及事件配置
@Slf4j
@Configuration
@EnableStateMachineFactory(name="exampleStateMachineFactory")
public class ExampleStateMachineFactoryConfig extends EnumStateMachineConfigurerAdapter<States, Events>{
@Resource
private ExampleGuard exampleGuard;
@Resource
private ExampleAction exampleAction;
@Resource
private ExampleStateMachinePersist exampleStateMachinePersist;
//初始态设置及状态集设置
@Override
public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception {
states.withStates()
//定义初始状态
.initial(States.S1)
//定义终结状态
.end(States.S6)
//定义选择分支
.choice(States.S3)
.states(EnumSet.allOf(States.class));
}
//状态转移设置
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception {
transitions
.withExternal()
.source(States.S1)
.target(States.S2)
.event(Events.E1)
.and()
.withExternal()
.source(States.S2)
.target(States.S3)
.event(Events.E2)
.and()
.withChoice()
.source(States.S3)
.first(States.S4, exampleGuard, exampleAction)
.last(States.S5, exampleAction)
.and()
.withExternal()
.source(States.S4)
.target(States.S6)
.event(Events.E3)
.and()
.withExternal()
.source(States.S5)
.target(States.S6)
.event(Events.E4)
.and();
}
@Bean(name="examplePersister")
public StateMachinePersister<States, Events, ExampleMessage> orderPersister() {
return new DefaultStateMachinePersister<>(exampleStateMachinePersist);
}
}
在以上代码中我们使用工厂状态机工厂方式开启了状态机,并且初始化了一系列的状态以及状态之间的转换生效的事件,需要注意的内容如下:
采用枚举形式定义配置EnumStateMachineConfigurerAdapter,方便而且友好
涉及到状态流转的分支必须在withStates().choice中定义
涉及到状态流转的分支在事件配置中需要用withChoice进行配置
withChoice中的状态流转触发的事件需要在action中定义
withChoice的生效需要guard控制
图2-1描述了这种转换关系
第三步-事件监听器以及拦截器的使用配置
/**
* 状态机拦截器
*/
@Slf4j
@Component
public class ExampleStateMachineInterceptor extends StateMachineInterceptorAdapter<States, Events> {
@Override
public Exception stateMachineError(StateMachine<States, Events> stateMachine, Exception exception) {
//此处处理相关的异常
//获取当前的状态或者是参数
stateMachine.getState().getId();
return exception;
}
}
/**
* 事件监听器
*/
@Slf4j
@WithStateMachine(name="exampleStateMachine")
public class ExampleFactoryEventConfig {
/**
* S1->S2
*/
@OnTransition(source = "S1", target = "S2")
public void pay(StateMachine<States, Events> stateMachine, Message<OrderEvents> message) {
try{
//具体的业务逻辑
//获取参数
ExampleMessage exampleMessage = message.getHeaders().get("exampleMessage", ExampleMessage.class);
}catch (Exception ex){
//通知状态机出错了
stateMachine.setStateMachineError(ex);
}
}
}
以上代码定义了状态机状态变化触发的具体业务方法以及公共的拦截器,需要注意的内容如下:
只有当stateMachine.setStateMachineError(ex);触发时,拦截器的stateMachineError方法才会生效,在stateMachineError中我们可以获取到当前的参数以及状态,并且依据这些来做一些业务处理
配置中的withChoice方式不会触发ExampleFactoryEventConfig中定义的状态流转方法,所以我们在定义withChoice时指定了exampleAction,相关的业务逻辑会在具体的action中生效
@WithStateMachine中具体的name属性为状态机名(具体的状态机名定义请见“第五步-具体使用”)
第四步-监视条件(guard)与动作(action)的配置
/**
* 监视条件,与withChoice一起使用
*/
@Component
public class ExampleGuard implements Guard<States, Events> {
@Override
public boolean evaluate(StateContext stateContext) {
//获取传递的message(参数)
ExampleMessage exampleMessage = stateContext.getMessage().getHeaders().get("exampleMessage", ExampleMessage.class);
//获取当前在执行的状态机
stateContext.getStateMachine();
//根据具体的exampleMessage中的值或者是其他具体的业务逻辑去决定流程的走向
return false;
}
}
/**
* 示例Action
*/
@Component
public class ExampleAction implements Action<States, Events> {
@Override
public void execute(StateContext<States, Events> stateContext) {
//获取传递的message(参数)
ExampleMessage exampleMessage = stateContext.getMessage().getHeaders().get("exampleMessage", ExampleMessage.class);
//获取当前在执行的状态机
stateContext.getStateMachine();
//执行具体的业务逻辑等
}
}
以上代码演示了监视器以及动作的定义,在这两处都是可以获取到对应的状态机以及message信息。不同的是监视器中我们做的操作一般是通过某些条件去判断是否要触发某些状态,而动作中的操作一般为具体的业务动作。
第五步-具体使用
/**
* 状态机触发器
*/
@Slf4j
@Component("exampleStateMachineTrigger")
public class ExampleStateMachineTrigger {
@Resource(name = "exampleStateMachineFactory")
private StateMachineFactory<States, Events> exampleStateMachineFactory;
@Resource(name = "examplePersister")
private StateMachinePersister<States, Events, ExampleMessage> examplePersister;
@Resource
private ExampleStateMachineInterceptor exampleStateMachineInterceptor;
/**
* 发送事件,触发状态转换
* @param param 传递的业务参数,非空
* @param event 发送的事件,非空
* @param restoreStatus 要重置到的目标状态
*/
public void sendEvent(ExampleMessage param, Events event, States restoreStatus) throws Exception {
StateMachine<States, Events> exampleStateMachine = exampleStateMachineFactory.getStateMachine("exampleStateMachine");
param.setState(restoreStatus.toString());
//重置到目标状态
examplePersister.restore(exampleStateMachine, param);
//set interceptor
exampleStateMachine.getStateMachineAccessor().doWithRegion(access -> access.addStateMachineInterceptor(exampleStateMachineInterceptor));
exampleStateMachine.sendEvent(MessageBuilder.withPayload(event).setHeader("exampleMessage", param).build());
//查看状态机是否出错
exampleStateMachine.hasStateMachineError();
}
}
以上代码演示了状态机的使用,sendEvent方法是核心,下面我们来说明一下具体的内容
examplePersister.restore持久化器的使用参见“状态机持久化器”
exampleStateMachineFactory的获取来自于第一步中定义@EnableStateMachineFactory的name属性
MessageBuilder构建时需要指定payLoad为具体的事件,setHeader中定义要传递的参数
exampleStateMachine.hasStateMachineError()表示状态机在执行时是否有错误,来源于状态变化时手动塞入的异常stateMachine.setStateMachineError(ex);
exampleStateMachineFactory.getStateMachine(“exampleStateMachine”);方法参数为状态机名,这里的状态机名也是@WithStateMachine注解中name的属性值
状态机持久化器
在真正的业务开发中,我们不可能每次都从起始状态开始,更多的场景是我们触发一个事件然后就开始业务逻辑了,或者是状态机根据业务场景在某一个状态停留直到下一个事件触发转换,而Spring StateMachine给出的方案是利用redis将状态机给保存起来,需要用时再根据业务id去还原。
具体的redis状态机持久化器我们可以去看官网,目前内容平台-题库系统采用的是根据指定的状态利用持久化器restore方法去还原,并不做持久化操作,具体代码如下:
@Component
public class ExampleStateMachinePersist implements StateMachinePersist<States, Events, ExampleMessage> {
@Override
public void write(StateMachineContext<States, Events> context, ExampleMessage contextObj) throws Exception {
//这里不做任何持久化工作
}
@Override
public StateMachineContext<States, Events> read(ExampleMessage contextObj) throws Exception {
return new DefaultStateMachineContext<>(States.valueOf(contextObj.getState()),
null, null, null, null, "exampleStateMachine");
}
}
注意项如下:
read方法参数中exampleStateMachine代表了具体的状态机名。
为了能够恢复到具体的某一个状态,我们尽量在ExampleMessage中设置一个属性来表示,示例代码中用state属性来表示。
目前系统中使用Spring StateMachine还只是单机的事件传播机制,并且对事件溯源等问题还未进行研究,后续我们会加入一些领域模型、事件溯源(现阶段调研初步为Axon)等框架。以此针对领域思想、事件溯源、分布式事件、分布式事务等作出更加深入的研究与实践。