<返回更多

Spring Statemachine应用实践

2023-04-12  微信公众号  之家技术
加入收藏

前言 

在日常开发中经常遇到运营审核经销商活动、任务等等类似业务需求,大部分需求中状态稳定且单一无需使用状态机,但是也会出现大量的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

<?xml versinotallow="1.0" encoding="utf-8" ?>
<!-- spring statemachine -->
<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-starter</artifactId>
    <version>2.2.0.RELEASE</version>
</dependency>
        <!-- spring statemachine context 序列化 -->
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-kryo</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
        <!-- squirrel-foundation -->
<dependency>
<groupId>org.squirrelframework</groupId>
<artifactId>squirrel-foundation</artifactId>
<version>0.3.10</version>
</dependency>

状态&事件定义

 
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<State, Event> {

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired
    private StateMachineRuntimePersister<State, Event, String> activityStateMachinePersister;

    @Bean
    public StateMachineService<State, Event> activityStateMachineService(StateMachineFactory<State, Event> stateMachineFactory) {

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

    @Override
    public void configure(StateMachineConfigurationConfigurer<State, Event> 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<State, Event> states) throws Exception {
        states.withStates()
                .initial(State.INIT)
                .choice(State.OFFLINING)
                .states(EnumSet.allOf(State.class));
    }

    @Override
    public void configure(StateMach.NETransitionConfigurer<State, Event> 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<State, Event>() {
                    @Override
                    public boolean evaluate(StateContext<State, Event> context) {
                        return true;
                    }
                })
                .last(State.OFFLINED);
        // @formatter:on
    }
}

说明:

Guard与Action

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

@Component
public class SaveAction implements Action<State, Event> {

    @Override
    public void execute(StateContext<State, Event> 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<State, Event, String> {

    @Autowired
    private ActivityStateService activityStateService;

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

    @Override
    public StateMachineContext<State, Event> read(String id) {
        return deserialize(activityStateService.getContextById(id));
    }
}

说明:

状态服务调用

 
@Service
public class StateTransitService {

    @Autowired
    private StateMachineService<State, Event> stateMachineService;

    @Transactional
    public void transimit(String machineId, Message<Event> message) {
        StateMachine<State, Event> 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<S, E> extends StateMachineListenerAdapter<S, E> {

    private final StateMachine<S, E> stateMachine;

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

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

说明:

集成单元测试

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

}

注意事项

扩展-与squirrel-foundation异同

@Component
public class ActivityMachine extends SquirrelStateMachine<ActivityMachine, State, Event, TransmitCmd> {

    private final ActivityStateService activityStateService;

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

    @Override
    public void buildStateMachine(StateMachineBuilder<ActivityMachine, State, Event, TransmitCmd> 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<ActivityMachine, State, Event, TransmitCmd>() {

        });
        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);
    }

}

说明:

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

声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>