Spring Statemachine应用实践

这篇具有很好参考价值的文章主要介绍了Spring Statemachine应用实践。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Spring Statemachine应用实践

 前言 

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

 

1. 什么是状态机?

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

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

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

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

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

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

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

     

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

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

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

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

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

     

其他术语:

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

  • source/from:现态。

  • target/to:次态。

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

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

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

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

Spring Statemachine应用实践

 

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。这里仅供使用对比。

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

 

Spring Statemachine应用实践

pom

<?xml version="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(StateMachineTransitionConfigurer<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
    }
}

说明:

  1. 多个状态节点配置可用.and()串联。

  2. withExternal是当现态和次态不相同时使用。

  3. withChoice是当执行一个动作,当前状态(瞬时状态)可能迁移不同的的状态,此时可以使用Choice和Guard组合使用,且无需事件触发。相当于if...else的分支状态功能。

  4. StateMachineService 这个类是spring statemachine自带的接口,用于获取和释放一个状态机的辅助service,依赖状态机工厂和持久化 实例, 但由于默认实现 StateMachinePersist< S, E, String> 规定了StateMachineContext的泛型为String 类型,故而持久层的参数contextObj 为string 类型,实际是状态机的id。

  5. 持久化 spring-statemachine官方支持MongoDB和Redis持久化存储,开发无需关心状态持久化,但是存在业务数据存储和状态存储事务的问题, 这里需要自己实现(StateMachineRuntimePersister)持久化以存储状态。

  6. 上下文传递时都使用的StateMachineContext,其内部包含StateMachine实例,可以通过增加StateMachine实例扩展参数传递参数。

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

说明:

  1. AbstractStateMachineRuntimePersister 继承AbstractPersistingStateMachineInterceptor 并实现了StateMachineRuntimePersister接口, AbstractPersistingStateMachineInterceptor主要拦截状态变更时的状态监听。不同于StateMachineListener被动监听,interceptor拥有可以改变状态变化链的能力。

  2. 序列化存储实现参考了spring-statemachine-data-redis的实现。

状态服务调用
@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());
    }
}

说明:

  1. Message为发送事件的载体,其内部封装了消息体、事件等上下文扩展参数。

  2. StateMachineListenerAdapter为默认监听接口的空实现,依据业务需要重写监听的方法。

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

}

 

注意事项

  1. 由于框架中每次都是加载一个状态机内存实例,所以在执行状态转移相关代码时一定要加分布式锁!!!建议状态维护提供统一调用service, 开启事务、处理异常。

  2. spring-statemachine异常包装比较另类,如guard、action以及listener中发生异常,状态机会捕获并把异常信息捕获为警告,状态也能够成功转移到次态,这显然不符合 我们的需求,所以调用后需要手动判断是否发生异常stateMachine.hasStateMachineError(),但statemachine并没有给提供获取异常信息的接口,所以在guard 和action中将异常信息用变量的方式解决此问题,stateMachine.getExtendedState().getVariables().put("message", "当前状态不满足执行条件");

  3. @EnableStateMachineFactory开启工厂模式,然后通过StateMachineService从持久化层加载一个状态机实例。

  4. 当一个project中有多个业务状态机时,@EnableStateMachineFactory(name = "xxx")为工厂配置名称以区别不同的业务状态机。

  5. 当使用withChoice()时,一定要在配置StateMachineStateConfigurer.choice()配置分支状态,否则将不生效。

 

扩展-与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);
    }

}

说明:

  1. squirrel-foundation直接可继承AbstractStateMachine实例化状态机,配置上大体相同只是使用的是from、to、on、when词不同,框架builder的约束太强。

  2. 不支持choice分支状态。

  3. 状态机异常处理afterTransitionCausedException相比spring-statemachine更加方便、易用。

  4. 状态的持久化通过重写afterTransitionCompleted方法即可。

     

5.使用后的效果如何?

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

1.由于新项目中涉及到跨部门卡券业务,在开发初期审核活动通过时同步创建卡券批次,却忽略了异步生成券码的时间,随着开发的深入才意识到此问题。此时只需要在状态审核通过时加一个过渡状态并启动一个任务去轮询券码是否创建完成即可,丝毫不影响已开发的代码。

2.最初的需求设计时,活动下线后是不能再次上线的,在需求迭代期内又增加了再次上线的功能,状态机流转逻辑清晰,只需要再增加个状态配置流转事件就行,就为状态机赋予了再次上线的能力。

 

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/

作者|姜强强文章来源地址https://www.toymoban.com/news/detail-445447.html

到了这里,关于Spring Statemachine应用实践的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Spring Boot 工程开发常见问题解决方案,日常开发全覆盖

    本文是 SpringBoot 开发的干货集中营,涵盖了日常开发中遇到的诸多问题,通篇着重讲解如何快速解决问题,部分重点问题会讲解原理,以及为什么要这样做。便于大家快速处理实践中经常遇到的小问题,既方便自己也方便他人,老鸟和新手皆适合,值得收藏 😄 https://mvnrepo

    2024年03月27日
    浏览(64)
  • 《Git入门实践教程》前言+目录

    版本控制系统(VCS)在项目开发中异常重要,但和在校大学生的交流中知道,这个重要方向并未受到重视。具备这一技能,既是项目开发能力的体现,也可为各种面试加码。在学习体验后知道,Git多样化平台、多种操作方式、丰富的资源为业内人士提供了方便的同时,也造成

    2024年02月10日
    浏览(72)
  • FPGA学习实践之旅——前言及目录

    很早就有在博客中记录技术细节,分享一些自己体会的想法,拖着拖着也就到了现在。毕业至今已经半年有余,随着项目越来越深入,感觉可以慢慢进行总结工作了。趁着2024伊始,就先开个头吧,这篇博客暂时作为汇总篇,记录在这几个月以及之后从FPGA初学者到也算有一定

    2024年02月03日
    浏览(58)
  • Spring Boot 缓存应用实践

    缓存是最直接有效提升系统性能的手段之一。个人认为用好用对缓存是优秀程序员的必备基本素质。本文结合实际开发经验,从简单概念原理和代码入手,一步一步搭建一个简单的二级缓存系统。 1、缓存基础算法 FIFO(First In First Out) ,先进先出,和OS里的FIFO思路相同,如

    2024年02月13日
    浏览(55)
  • Spring MVC 深度解析与应用实践

    Spring MVC(Model-View-Controller)是 Spring 框架的一部分,专门用于设计创建分层的 Java Web 应用。它是一个全功能的 MVC 模块,能够提供强大的配置选项,并利用默认的约定,使基本项目的配置降至最低。Spring MVC 提供了一种分离的方式,使得能够通过控制器(Controller)进行业务处

    2024年02月11日
    浏览(47)
  • 从零开始学Spring Boot系列-前言

    在数字化和信息化的时代,Java作为一种成熟、稳定且广泛应用的编程语言,已经成为构建企业级应用的首选。而在Java生态系统中,Spring框架无疑是其中最为耀眼的一颗明星。它提供了全面的编程和配置模型,用于构建企业级应用。随着Spring Boot的出现,这一框架变得更加易于

    2024年02月22日
    浏览(59)
  • Spring Boot项目中TaskDecorator的应用实践

    TaskDecorator是一个执行回调方法的装饰器,主要应用于传递上下文,或者提供任务的监控/统计信息,可以用于处理子线程与主线程间数据传递的问题。 1.自定义TaskDecorator 2. 自定义线程池,设置TaskDecorator 3. 测试

    2024年02月21日
    浏览(29)
  • 【Java】保护你的应用:深入探讨Spring Security的应用与最佳实践

    人不走空                                                                        在当今数字化时代,信息安全已成为应用开发中至关重要的一环。Spring Security作为Spring生态系统中的一个关键组件,为应用提供了强大的身份验证和访问控制功能。本文将深入探讨Spr

    2024年02月20日
    浏览(53)
  • Spring高手之路4——深度解析Spring内置作用域及其在实践中的应用

    我们来看看 Spring 内置的作用域类型。在 5.x 版本中, Spring 内置了六种作用域: singleton :在 IOC 容器中,对应的 Bean 只有一个实例,所有对它的引用都指向同一个对象。这种作用域非常适合对于无状态的 Bean ,比如工具类或服务类。 prototype :每次请求都会创建一个新的 Be

    2024年02月08日
    浏览(42)
  • Spring Cloud开发实践(七): 集成Consul配置中心

    Spring Cloud开发实践(一): 简介和根模块 Spring Cloud开发实践(二): Eureka服务和接口定义 Spring Cloud开发实践(三): 接口实现和下游调用 Spring Cloud开发实践(四): Docker部署 Spring Cloud开发实践(五): Consul - 服务注册的另一个选择 Spring Cloud开发实践(六): 基于Consul和Spring Cloud 2021.0的演示项目

    2024年02月07日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包