SpringStatemachine应用实践

前言 

在日常开发中经常遇到运营审核经销商活动、任务等等类似业务需求,大部分需求中状态稳定且单一无需使用状态机,但是也会出现大量的if...else前置状态代码,也是不够那么的“优雅”。随着业务的发展、需求迭代,每一次的业务代码改动都需要维护使用到状态的代码,更让开发人员头疼的是这些维护状态的代码,像散弹一样遍布在各个Service的方法中,不仅增加发布的风险,同时也增加了回归测试的工作量。

1. 什么是状态机?

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

应用FSM模型可以帮助对象生命周期的状态的顺序以及导致状态变化的事件进行管理。 将状态和事件控制从不同的业务Service方法的if else中抽离出来。FSM的应用范围很广,状态机 可以描述核心业务规则,核心业务内容. 无限状态机,顾名思义状态无限,类似于“π”,暂不做研究。

状态机可归纳为4个要素,即现态、条件、动作、次态。这样的归纳,主要是出于对状态机的内在因果关系的考虑。“现态”和“条件”是因,“动作”和“次态”是果。详解如下:

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

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

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

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

动作是在给定时刻要进行的活动的描述。有多种类型的动作:

进入动作(entry action):在进入状态时进行

退出动作(exit action):在退出状态时进行

输入动作:依赖于当前状态和输入条件进行

转移动作:在进行特定转移时进行

其他术语:

Transition: 状态转移节点,是组成状态机引擎的核心。

source/from:现态。

target/to:次态。

event/trigger:触发节点从现态转移到次态的动作,这里也可能是一个timer。

guard/when:状态迁移前的校验,执行于action前。

action:用于实现当前节点对应的业务逻辑处理。

文字描述比较不容易理解,让我们举个栗子:每天上班都需要坐地铁,从刷卡进站到闸机关闭这个过程,将闸机抽象为一个状态机模型,如下图:

2. 什么场景使用?

以下的场景您可能会需要使用:

您可以将应用程序或其结构的一部分表示为状态。

您希望将复杂的逻辑拆分为更小的可管理任务。

应用程序已经遇到了并发问题,例如异步执行导致了一些异常情况。

当您执行以下操作时,您已经在尝试实现状态机:

使用布尔标志或枚举来建模情况。

具有仅对应用程序生命周期的某些部分有意义的变量。

在if...else结构(或者更糟糕的是,多个这样的结构)中循环,检查是否设置了特定的标志或枚举,然后在标志和枚举的某些组合存在或不存在时,做出进一步的异常处理。

3. 为什么要用?有哪些好处?

最初活动模块功能设计时,并没有想使用状态机,仅仅想把状态的变更和业务剥离开,规范状态转换和程序在不同状态下所能提供的能力,去掉复杂的逻辑判断也就是if...else,想换一种模式实现思路,此前了解过spring“全家桶”有状态机就想到了“它”,场景也符合。

从个人使用的经验,开发阶段和迭代维护期总结了以下几点:

使用状态机来管理状态好处更多体现在代码的可维护性、对于流程复杂易变的业务场景能大大减轻维护和测试的难度。

解耦,业务逻辑与状态流程隔离,避免业务与状态“散弹式”维护,且状态持久化在同一个事务。

状态流转越复杂,越能体现状态流转的逻辑清晰,减少的“胶水”代码也越多。

4. 实践

java语言状态机框架有很多,目前github star 数比较多的有 spring-statemachine(star 1.3K) 、squirrel-foundation(star1.9K)即“松鼠”状态机,stateless4j相较前两个名气较小,未深入研究。spring-statemachine是spring官方提供的状态机实现,功能强大,但是相对来说很“重”,加载实例的时间也长于squirrel-foundation,不过好在一直都是有更新(目前官方已更新3.2.0),相信会越来越成熟。

实际生产中使用的是spring statemachine ,版本是2.2.0.RELEASE。线下对比使用的是squirrel-foundation,版本是0.3.10。这里仅供使用对比。

从创建活动到活动下线状态流转作为示例,如下图:

pom




org.springframework.statemachine
spring-statemachine-starter
2.2.0.RELEASE



org.springframework.statemachine
spring-statemachine-kryo
2.2.0.RELEASE



org.squirrelframework
squirrel-foundation
0.3.10

状态&事件定义

public enum State {
INIT("初始化"),
DRAFT("草稿"),
WAIT_VERIFY("待审核"),
PASSED("审核通过"),
REJECTED("已驳回"),
//已发起上线操作,未到上线时间的状态
WAIT_ONLIE("待上线"),
ONLINED("已上线"),
//过渡状态无实际意义,无需事件触发
OFFLINING("下线中"),
OFFLINED("已下线"),
FINISHED("已结束");
private final String desc;
}

public enum Event {
SAVE("保存草稿"),
SUBMIT("提交审核"),
PASS("审核通过"),
REJECT("提交驳回"),
ONLINE("上线"),
OFFLINE("下线"),
FINISH("结束");
private final String desc;
}

状态流转定义

@Configuration
@EnableStateMachineFactory
public class ActivitySpringStateMachineAutoConfiguration extends StateMachineConfigurerAdapter {

@Autowired
private ApplicationContext applicationContext;

@Autowired
private StateMachineRuntimePersister activityStateMachinePersister;

@Bean
public StateMachineService activityStateMachineService(StateMachineFactory stateMachineFactory) {

return new DefaultStateMachineService<>(stateMachineFactory, activityStateMachinePersister);
}

@Override
public void configure(StateMachineConfigurationConfigurer config) throws Exception {
// @formatter:off
config
.withPersistence()
.runtimePersister(activityStateMachinePersister)
.and().withConfiguration()
.stateDoActionPolicy(StateDoActionPolicy.TIMEOUT_CANCEL)
.stateDoActionPolicyTimeout(300, TimeUnit.SECONDS)
.autoStartup(false);
// @formatter:on
}

@Override
public void configure(StateMachineStateConfigurer states) throws Exception {
states.withStates()
.initial(State.INIT)
.choice(State.OFFLINING)
.states(EnumSet.allOf(State.class));
}

@Override
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
// 待提交审核 --提交审核--> 待审核
// @formatter:off
// 现态-->事件-->次态
transitions.withExternal()
.source(State.INIT).target(State.DRAFT).event(Event.SAVE)
.and().withExternal()
.source(State.DRAFT).target(State.WAIT_VERIFY).event(Event.SUBMIT)
.guard(applicationContext.getBean(SubmitCondition.class));
transitions.withExternal().source(State.WAIT_VERIFY).target(State.PASSED).event(Event.PASS)
.action(applicationContext.getBean(PassAction.class));
transitions.withExternal().source(State.WAIT_VERIFY).target(State.REJECTED).event(Event.REJECT)
.guard(applicationContext.getBean(RejectCondition.class));
transitions.withExternal()
.source(State.REJECTED)
.target(State.WAIT_VERIFY)
.event(Event.SUBMIT)
.guard(applicationContext.getBean(SubmitCondition.class));

// 审核通过-->上线-->待上线
transitions.withExternal().source(State.PASSED).target(State.WAIT_ONLIE).event(Event.ONLINE);
// 待上线-->上线-->已上线
transitions.withExternal().source(State.WAIT_ONLIE).target(State.ONLINED).event(Event.ONLINE);
// 已上线-->下线-->已下线
transitions.withExternal()
.source(State.ONLINED).target(State.OFFLINING).event(Event.OFFLINE);
// 待上线-->下线-->下线中
transitions.withExternal()
.source(State.WAIT_ONLIE).target(State.OFFLINING).event(Event.OFFLINE)
.and()
// 已下线-->结束-->已结束
.withChoice()
.source(State.OFFLINING)
.first(State.FINISHED, new Guard() {
@Override
public boolean evaluate(StateContext context) {
return true;
}
})
.last(State.OFFLINED);
// @formatter:on
}
}

说明:

  • 多个状态节点配置可用.and()串联。
  • withExternal是当现态和次态不相同时使用。
  • withChoice是当执行一个动作,当前状态(瞬时状态)可能迁移不同的的状态,此时可以使用Choice和Guard组合使用,且无需事件触发。相当于if...else的分支状态功能。
  • StateMachineService 这个类是spring statemachine自带的接口,用于获取和释放一个状态机的辅助service,依赖状态机工厂和持久化 实例, 但由于默认实现 StateMachinePersist< S, E, String> 规定了StateMachineContext的泛型为String 类型,故而持久层的参数contextObj 为string 类型,实际是状态机的id。
  • 持久化 spring-statemachine官方支持MongoDB和Redis持久化存储,开发无需关心状态持久化,但是存在业务数据存储和状态存储事务的问题, 这里需要自己实现(StateMachineRuntimePersister)持久化以存储状态。
  • 上下文传递时都使用的StateMachineContext,其内部包含StateMachine实例,可以通过增加StateMachine实例扩展参数传递参数。

Guard与Action

@Component
public class SaveGuard implements Guard {
@Override
public boolean evaluate(StateContext context) {
log.info("[execute save guard]");
return true;
}
}

@Component
public class SaveAction implements Action {

@Override
public void execute(StateContext context) {
try {
log.info("[execute saveAction]");
} catch (Exception e) {
context.getExtendedState().getVariables().put("ERROR", e.getMessage());
}
}
}

说明:

  1. Guard 门卫,条件判断返回true时再执行状态转移,可以做业务前置校验。

持久化配置

@Component
public class ActivityStateMachinePersister extends AbstractStateMachineRuntimePersister {

@Autowired
private ActivityStateService activityStateService;

@Override
public void write(StateMachineContext context, String id) {
Activity state = new Activity();
state.setMachineId(id);
state.setState(context.getState());
activityStateService.save(state);
}

@Override
public StateMachineContext read(String id) {
return deserialize(activityStateService.getContextById(id));
}
}

说明:

  • AbstractStateMachineRuntimePersister 继承AbstractPersistingStateMachineInterceptor 并实现了StateMachineRuntimePersister接口, AbstractPersistingStateMachineInterceptor主要拦截状态变更时的状态监听。不同于StateMachineListener被动监听,interceptor拥有可以改变状态变化链的能力。
  • 序列化存储实现参考了spring-statemachine-data-redis的实现。

状态服务调用

@Service
public class StateTransitService {

@Autowired
private StateMachineService stateMachineService;

@Transactional
public void transimit(String machineId, Message message) {
StateMachine stateMachine = stateMachineService.acquireStateMachine(machineId);
stateMachine.addStateListener(new DefaultStateMachineListener<>(stateMachine));
stateMachine.sendEvent(message);
if (stateMachine.hasStateMachineError()) {
String errorMessage = stateMachine.getExtendedState().get("message", String.class);
stateMachineService.releaseStateMachine(machineId);
throw new ResponseException(errorMessage);
}
}
}

@AllArgsConstructor
public class DefaultStateMachineListener extends StateMachineListenerAdapter {

private final StateMachine stateMachine;

@Override
public void eventNotAccepted(Message event) {
stateMachine.getExtendedState().getVariables().put("message", "当前状态不满足执行条件");
stateMachine.setStateMachineError(new ResponseException(500, "Event not accepted"));
}

@Override
public void transitionEnded(Transition transition) {
log.info("source {} to {}", transition.getSource().getId(), transition.getTarget().getId());
}
}

说明:

  • Message为发送事件的载体,其内部封装了消息体、事件等上下文扩展参数。
  • StateMachineListenerAdapter为默认监听接口的空实现,依据业务需要重写监听的方法。
  • eventNotAccepted此为事件未正确执行时的监听器。

集成单元测试

@SpringBootTest
@RunWith(SpringRunner.class)
public class StateMachineITest {

@Autowired
private StateTransitService transmitService;

@Autowired
private ActivityStateService activityStateService;

@Test
public void test() {
String machineId = "test";//业务主键ID
transmitService.transimit(machineId, MessageBuilder.withPayload(Event.SAVE).build());
transmitService.transimit(machineId, MessageBuilder.withPayload(Event.SUBMIT).build());
transmitService.transimit(machineId, MessageBuilder.withPayload(Event.PASS).build());
transmitService.transimit(machineId, MessageBuilder.withPayload(Event.ONLINE).build());
transmitService.transimit(machineId, MessageBuilder.withPayload(Event.ONLINE).build());
transmitService.transimit(machineId, MessageBuilder.withPayload(Event.OFFLINE).build());
assert activityStateService.getStateById(machineId).equals(State.FINISHED);
}

}

注意事项

  • 由于框架中每次都是加载一个状态机内存实例,所以在执行状态转移相关代码时一定要加分布式锁!!!建议状态维护提供统一调用service, 开启事务、处理异常。
  • spring-statemachine异常包装比较另类,如guard、action以及listener中发生异常,状态机会捕获并把异常信息捕获为警告,状态也能够成功转移到次态,这显然不符合 我们的需求,所以调用后需要手动判断是否发生异常stateMachine.hasStateMachineError(),但statemachine并没有给提供获取异常信息的接口,所以在guard 和action中将异常信息用变量的方式解决此问题,stateMachine.getExtendedState().getVariables().put("message", "当前状态不满足执行条件");
  • @EnableStateMachineFactory开启工厂模式,然后通过StateMachineService从持久化层加载一个状态机实例。
  • 当一个project中有多个业务状态机时,@EnableStateMachineFactory(name = "xxx")为工厂配置名称以区别不同的业务状态机。
  • 当使用withChoice()时,一定要在配置StateMachineStateConfigurer.choice()配置分支状态,否则将不生效。

扩展-与squirrel-foundation异同

@Component
public class ActivityMachine extends SquirrelStateMachine {

private final ActivityStateService activityStateService;

public ActivityMachine(ApplicationContext applicationContext) {
super(applicationContext);
activityStateService = applicationContext.getBean(ActivityStateService.class);
}

@Override
public void buildStateMachine(StateMachineBuilder stateMachineBuilder) {
stateMachineBuilder.externalTransition().from(State.INIT).to(State.DRAFT).on(Event.SAVE).when(applicationContext.getBean(SubmitCondition.class));
//以下省略,大致与spring-statemachine相同
}

@Override
public ActivityMachine createStateMachine(State stateId) {
ActivityMachine activityMachine = super.createStateMachine(stateId);
activityMachine.addStartListener(new StartListener() {

});
return activityMachine;
}

@Override
protected void afterTransitionDeclined(S fromState, E event, C context) {
//转移状态未执行
}

@Override
protected void afterTransitionCausedException(S fromState, S toState, E event, C context) {
// 转移状态时发生异常
}

@Override
protected void afterTransitionCompleted(State fromState, State toState, Event event, TransmitCmd context) {
log.info("from {} to {} on {}, {}", fromState.getDesc(), toState.getDesc(), event.getDesc(), context);
}

}

说明:

  • squirrel-foundation直接可继承AbstractStateMachine实例化状态机,配置上大体相同只是使用的是from、to、on、when词不同,框架builder的约束太强。
  • 不支持choice分支状态。
  • 状态机异常处理afterTransitionCausedException相比spring-statemachine更加方便、易用。
  • 状态的持久化通过重写afterTransitionCompleted方法即可。

5.使用后的效果如何?

以下是在开发和迭代维护期间,真切体会到状态机带来好处的两个小场景。

  • 由于新项目中涉及到跨部门卡券业务,在开发初期审核活动通过时同步创建卡券批次,却忽略了异步生成券码的时间,随着开发的深入才意识到此问题。此时只需要在状态审核通过时加一个过渡状态并启动一个任务去轮询券码是否创建完成即可,丝毫不影响已开发的代码。
  • 最初的需求设计时,活动下线后是不能再次上线的,在需求迭代期内又增加了再次上线的功能,状态机流转逻辑清晰,只需要再增加个状态配置流转事件就行,就为状态机赋予了再次上线的能力。

6.总结 

在实践的过程中,在spring-statemachine官方文档结合Google摸索使用的过程中,遇到持久化存储StateMachineContext、异常处理,以及状态分支等问题。目前回头看来也不复杂,如今写出来总结一下,希望对小伙伴们有所帮助。

最后建议在状态流程不是很复杂的情况,如果您也厌烦了if...else,那么不妨尝试一下squirrel-foundation,相信也是不错的选择。

参考文献

  1. ​​https://baike.baidu.com/item/%E7%8A%B6%E6%80%81%E6%9C%BA/6548513?fr=aladdin​​
  2. ​​https://zh.wikipedia.org/wiki/%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E6%9C%BA​​
  3. ​​https://spring.io/projects/spring-statemachine#learn​​
  4. ​​http://hekailiang.github.io/squirrel/​​

作者简介

姜强强

■ 经销商技术部-商业资源团队。

■ 2016年加入汽车之家,目前主要负责经销商事业部内创新商业项目的研发工作,热衷于业内新技术的探索与实践。

网站题目:SpringStatemachine应用实践
分享URL:http://www.shufengxianlan.com/qtweb/news0/333050.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联