使用Spring StateMachine框架实现状态机

Spring StateMachine框架可能对于大部分使用Spring的开发者来说还比较生僻,该框架目前差不多也才刚满一岁多。它的主要功能是帮助开发者简化状态机的开发过程,让状态机结构更加层次化。前几天刚刚发布了它的第三个Release版本1.2.0,其中增加了对Spring Boot的自动化配置,既然一直在写Spring Boot的教程,所以干脆就将该内容也纳入进来吧,希望对有需求的小伙伴有一定的帮助。

快速入门

依照之前的风格,我们通过一个简单的示例来对Spring StateMachine有一个初步的认识。假设我们需要实现一个订单的相关流程,其中包括订单创建、订单支付、订单收货三个动作。

下面我们来详细的介绍整个实现过程:

  • 创建一个Spring Boot的基础工程,并在pom.xml中加入spring-statemachine-core的依赖,具体如下:

    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.3.7.RELEASE</version>
    <relativePath/>
    </parent>

    <dependencies>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-core</artifactId>
    <version>1.2.0.RELEASE</version>
    </dependency>
    </dependencies>
  • 根据上面所述的订单需求场景定义状态和事件枚举,具体如下:

    public enum States {
    UNPAID, // 待支付
    WAITING_FOR_RECEIVE, // 待收货
    DONE // 结束
    }

    public enum Events {
    PAY, // 支付
    RECEIVE // 收货
    }

    其中共有三个状态(待支付、待收货、结束)以及两个引起状态迁移的事件(支付、收货),其中支付事件PAY会触发状态从待支付UNPAID状态到待收货WAITING_FOR_RECEIVE状态的迁移,而收货事件RECEIVE会触发状态从待收货WAITING_FOR_RECEIVE状态到结束DONE状态的迁移。

  • 创建状态机配置类:

    @Configuration
    @EnableStateMachine
    public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<States, Events> {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void configure(StateMachineStateConfigurer<States, Events> states)
    throws Exception {
    states
    .withStates()
    .initial(States.UNPAID)
    .states(EnumSet.allOf(States.class));
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
    throws Exception {
    transitions
    .withExternal()
    .source(States.UNPAID).target(States.WAITING_FOR_RECEIVE)
    .event(Events.PAY)
    .and()
    .withExternal()
    .source(States.WAITING_FOR_RECEIVE).target(States.DONE)
    .event(Events.RECEIVE);
    }

    @Override
    public void configure(StateMachineConfigurationConfigurer<States, Events> config)
    throws Exception {
    config
    .withConfiguration()
    .listener(listener());
    }

    @Bean
    public StateMachineListener<States, Events> listener() {
    return new StateMachineListenerAdapter<States, Events>() {

    @Override
    public void transition(Transition<States, Events> transition) {
    if(transition.getTarget().getId() == States.UNPAID) {
    logger.info("订单创建,待支付");
    return;
    }

    if(transition.getSource().getId() == States.UNPAID
    && transition.getTarget().getId() == States.WAITING_FOR_RECEIVE) {
    logger.info("用户完成支付,待收货");
    return;
    }

    if(transition.getSource().getId() == States.WAITING_FOR_RECEIVE
    && transition.getTarget().getId() == States.DONE) {
    logger.info("用户已收货,订单完成");
    return;
    }
    }

    };
    }

    }

    在该类中定义了较多配置内容,下面对这些内容一一说明:

    • @EnableStateMachine注解用来启用Spring StateMachine状态机功能

    • configure(StateMachineStateConfigurer<States, Events> states)方法用来初始化当前状态机拥有哪些状态,其中initial(States.UNPAID)定义了初始状态为UNPAIDstates(EnumSet.allOf(States.class))则指定了使用上一步中定义的所有状态作为该状态机的状态定义。

      @Override
      public void configure(StateMachineStateConfigurer<States, Events> states)
      throws Exception {
      // 定义状态机中的状态
      states
      .withStates()
      .initial(States.UNPAID) // 初始状态
      .states(EnumSet.allOf(States.class));
      }
    • configure(StateMachineTransitionConfigurer<States, Events> transitions)方法用来初始化当前状态机有哪些状态迁移动作,其中命名中我们很容易理解每一个迁移动作,都有来源状态source,目标状态target以及触发事件event

      @Override
      public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
      throws Exception {
      transitions
      .withExternal()
      .source(States.UNPAID).target(States.WAITING_FOR_RECEIVE)// 指定状态来源和目标
      .event(Events.PAY) // 指定触发事件
      .and()
      .withExternal()
      .source(States.WAITING_FOR_RECEIVE).target(States.DONE)
      .event(Events.RECEIVE);
      }
    • configure(StateMachineConfigurationConfigurer<States, Events> config)方法为当前的状态机指定了状态监听器,其中listener()则是调用了下一个内容创建的监听器实例,用来处理各个各个发生的状态迁移事件。

      @Override
      public void configure(StateMachineConfigurationConfigurer<States, Events> config)
      throws Exception {
      config
      .withConfiguration()
      .listener(listener()); // 指定状态机的处理监听器
      }
    • StateMachineListener<States, Events> listener()方法用来创建StateMachineListener状态监听器的实例,在该实例中会定义具体的状态迁移处理逻辑,上面的实现中只是做了一些输出,实际业务场景会会有更负责的逻辑,所以通常情况下,我们可以将该实例的定义放到独立的类定义中,并用注入的方式加载进来。

  • 创建应用主类来完成整个流程:

    @SpringBootApplication
    public class Application implements CommandLineRunner {

    public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
    }

    @Autowired
    private StateMachine<States, Events> stateMachine;

    @Override
    public void run(String... args) throws Exception {
    stateMachine.start();
    stateMachine.sendEvent(Events.PAY);
    stateMachine.sendEvent(Events.RECEIVE);
    }

    }

    run函数中,我们定义了整个流程的处理过程,其中start()就是创建这个订单流程,根据之前的定义,该订单会处于待支付状态,然后通过调用sendEvent(Events.PAY)执行支付操作,最后通过掉用sendEvent(Events.RECEIVE)来完成收货操作。在运行了上述程序之后,我们可以在控制台中获得类似下面的输出内容:

    INFO 2312 --- [           main] eConfig$$EnhancerBySpringCGLIB$$a05acb3d : 订单创建,待支付
    INFO 2312 --- [ main] o.s.s.support.LifecycleObjectSupport : started org.springframework.statemachine.support.DefaultStateMachineExecutor@1d2290ce
    INFO 2312 --- [ main] o.s.s.support.LifecycleObjectSupport : started DONE UNPAID WAITING_FOR_RECEIVE / UNPAID / uuid=c65ec0aa-59f9-4ffb-a1eb-88ec902369b2 / id=null
    INFO 2312 --- [ main] eConfig$$EnhancerBySpringCGLIB$$a05acb3d : 用户完成支付,待收货
    INFO 2312 --- [ main] eConfig$$EnhancerBySpringCGLIB$$a05acb3d : 用户已收货,订单完成

    其中包括了状态监听器中对各个状态迁移做出的处理。

通过上面的例子,我们可以对如何使用Spring StateMachine做如下小结:

  • 定义状态和事件枚举
  • 为状态机定义使用的所有状态以及初始状态
  • 为状态机定义状态的迁移动作
  • 为状态机指定监听处理器

状态监听器

通过上面的入门示例以及最后的小结,我们可以看到使用Spring StateMachine来实现状态机的时候,代码逻辑变得非常简单并且具有层次化。整个状态的调度逻辑主要依靠配置方式的定义,而所有的业务逻辑操作都被定义在了状态监听器中,其实状态监听器可以实现的功能远不止上面我们所述的内容,它还有更多的事件捕获,我们可以通过查看StateMachineListener接口来了解它所有的事件定义:

public interface StateMachineListener<S,E> {

void stateChanged(State<S,E> from, State<S,E> to);

void stateEntered(State<S,E> state);

void stateExited(State<S,E> state);

void eventNotAccepted(Message<E> event);

void transition(Transition<S, E> transition);

void transitionStarted(Transition<S, E> transition);

void transitionEnded(Transition<S, E> transition);

void stateMachineStarted(StateMachine<S, E> stateMachine);

void stateMachineStopped(StateMachine<S, E> stateMachine);

void stateMachineError(StateMachine<S, E> stateMachine, Exception exception);

void extendedStateChanged(Object key, Object value);

void stateContext(StateContext<S, E> stateContext);

}

注解监听器

对于状态监听器,Spring StateMachine还提供了优雅的注解配置实现方式,所有StateMachineListener接口中定义的事件都能通过注解的方式来进行配置实现。比如,我们可以将之前实现的状态监听器用注解配置来做进一步的简化:

@WithStateMachine
public class EventConfig {

private Logger logger = LoggerFactory.getLogger(getClass());

@OnTransition(target = "UNPAID")
public void create() {
logger.info("订单创建,待支付");
}

@OnTransition(source = "UNPAID", target = "WAITING_FOR_RECEIVE")
public void pay() {
logger.info("用户完成支付,待收货");
}

@OnTransition(source = "WAITING_FOR_RECEIVE", target = "DONE")
public void receive() {
logger.info("用户已收货,订单完成");
}

}

上述代码实现了与快速入门中定义的listener()方法创建的监听器相同的功能,但是由于通过注解的方式配置,省去了原来事件监听器中各种if的判断,使得代码显得更为简洁,拥有了更好的可读性。

本文完整示例:

  • 开源中国:http://git.oschina.net/didispace/SpringBoot-Learning/tree/master/Chapter6-1-1
  • GitHub:https://github.com/dyc87112/SpringCloud-Learning/tree/master/1-Brixton%E7%89%88%E6%95%99%E7%A8%8B%E7%A4%BA%E4%BE%8B/Chapter6-1-1

money.jpg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/477261.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

滴滴退市了

文 | 彭小伟, 编 | 刘芳源 | 网约车焦点美东时间6月10日&#xff0c;滴滴正式在纽交所退市。根据官方通告&#xff0c;滴滴在完成退市后&#xff0c;其股份会转移到OTC&#xff08;Over-the-Counter&#xff0c;场外交易市场&#xff09;进行交易&#xff0c;交易代码为“DIDIY…

剑指Offer - 面试题62. 圆圈中最后剩下的数字(约瑟夫环 递推公式)

1. 题目 0,1,…,n-1这n个数字排成一个圆圈&#xff0c;从数字0开始&#xff0c;每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。 例如&#xff0c;0、1、2、3、4这5个数字组成一个圆圈&#xff0c;从数字0开始每次删除第3个数字&#xff0c;则删除的前4个…

Spring Boot中使用MyBatis注解配置详解

之前在Spring Boot中整合MyBatis时&#xff0c;采用了注解的配置方式&#xff0c;相信很多人还是比较喜欢这种优雅的方式的&#xff0c;也收到不少读者朋友的反馈和问题&#xff0c;主要集中于针对各种场景下注解如何使用&#xff0c;下面就对几种常见的情况举例说明用法。 在…

何恺明团队的“视频版本MAE”,高效视频预训练!Mask Ratio高达90%时效果也很好!...

文 | 小马源 | 我爱计算机视觉本篇文章分享论文『Masked Autoencoders As Spatiotemporal Learners』&#xff0c;由何恺明团队提出视频版本的 MAE&#xff0c;进行高效视频预训练&#xff01;Mask Ratio 高达 90% 时效果很好&#xff01;详细信息如下&#xff1a;论文链接&…

剑指Offer - 面试题45. 把数组排成最小的数(字符串排序)

1. 题目 输入一个正整数数组&#xff0c;把数组里所有数字拼接起来排成一个数&#xff0c;打印能拼接出的所有数字中最小的一个。 示例 1: 输入: [10,2] 输出: "102"示例 2: 输入: [3,30,34,5,9] 输出: "3033459"提示: 0 < nums.length < 100 说明…

Spring Boot整合MyBatis

最近项目原因可能会继续开始使用MyBatis&#xff0c;已经习惯于spring-data的风格&#xff0c;再回头看xml的映射配置总觉得不是特别舒服&#xff0c;接口定义与映射离散在不同文件中&#xff0c;使得阅读起来并不是特别方便。 Spring中整合MyBatis就不多说了&#xff0c;最近…

谷歌放弃Tensorflow,全面拥抱JAX

文 | Matthew Lynley源 | 机器之心编辑部TensorFlow 大概已经成为了谷歌的一枚「弃子」。2015 年&#xff0c;谷歌大脑开放了一个名为「TensorFlow」的研究项目&#xff0c;这款产品迅速流行起来&#xff0c;成为人工智能业界的主流深度学习框架&#xff0c;塑造了现代机器学习…

LeetCode 第 20 场双周赛(294 / 1541,前19.07%,第1次全部通过)

文章目录1. 比赛结果2. 题目LeetCode 5323. 根据数字二进制下 1 的数目排序 easyLeetCode 5324. 每隔 n 个顾客打折 mediumLeetCode 5325. 包含所有三种字符的子字符串数目 mediumLeetCode 5326. 有效的快递序列数目 hard1. 比赛结果 第一次全部做出来了&#xff0c;提前6分钟…

Spring Boot中的缓存支持(二)使用Redis做集中式缓存

上一篇介绍了在Spring Boot中如何引入缓存、缓存注解的使用、以及EhCache的整合。 虽然EhCache已经能够适用很多应用场景&#xff0c;但是由于EhCache是进程内的缓存框架&#xff0c;在集群模式下时&#xff0c;各应用服务器之间的缓存都是独立的&#xff0c;因此在不同服务器…

NLP开源数据集汇总

源 | 极市平台本文汇总了几个NLP相关的开源数据集&#xff0c;均附有下载链接。CASIA手写数据集数据集地址&#xff1a;http://m6z.cn/6pFPtCCASIA-HWDB-T&#xff1a;一个从中文手写数据库CASIA-HWDB收集的触摸字符数据库。所有接触的字符&#xff08;或字符串&#xff09;都用…

LeetCode 1360. 日期之间隔几天(闰年判断)

1. 题目 请你编写一个程序来计算两个日期之间隔了多少天。 日期以字符串形式给出&#xff0c;格式为 YYYY-MM-DD&#xff0c;如示例所示。 示例 1&#xff1a; 输入&#xff1a;date1 "2019-06-29", date2 "2019-06-30" 输出&#xff1a;1示例 2&…

“我要做小小瑶大人的狗!”

小轶&#xff1a;这真的是白鹡鸰写的作为21世纪新时代人格健全&#xff0c;精神独立&#xff0c;心理健康的有志青年&#xff0c;总有那么几个时刻&#xff0c;会让我们情不自禁&#xff0c;眼泪从嘴角流下地呐喊着&#xff1a;“我是XXX的狗&#xff01;”啊&#xff01;这诚恳…

Spring Boot中的缓存支持(一)注解配置与EhCache使用

随着时间的积累&#xff0c;应用的使用用户不断增加&#xff0c;数据规模也越来越大&#xff0c;往往数据库查询操作会成为影响用户使用体验的瓶颈&#xff0c;此时使用缓存往往是解决这一问题非常好的手段之一。Spring 3开始提供了强大的基于注解的缓存支持&#xff0c;可以通…

LeetCode 1361. 验证二叉树(图的出入度)

1. 题目 二叉树上有 n 个节点&#xff0c;按从 0 到 n-1 编号&#xff0c;其中节点 i 的两个子节点分别是 leftChild[i] 和 rightChild[i]。 只有 所有 节点能够形成且 只 形成 一颗 有效的二叉树时&#xff0c;返回 true&#xff1b;否则返回 false。 如果节点 i 没有左子节…

微软:我已把显存优化做到了极致,还有谁?

文 | 王思若大家好&#xff0c;我是王思若。17年6月Google提出了Transformer架构&#xff0c;这篇目前Citation 4.3万的文章开启了大规模预训练模型时代。或者&#xff0c;更精确的从18年OpenAI和Google分别基于其中的Decoder和Encoder发布的大规模预训练模型GPT1和BERT开始&am…

Spring Boot中使用JavaMailSender发送邮件

相信使用过Spring的众多开发者都知道Spring提供了非常好用的JavaMailSender接口实现邮件发送。在Spring Boot的Starter模块中也为此提供了自动化配置。下面通过实例看看如何在Spring Boot中使用JavaMailSender发送邮件。 快速入门 在Spring Boot的工程中的pom.xml中引入sprin…

LeetCode 1362. 最接近的因数

1. 题目 给你一个整数 num&#xff0c;请你找出同时满足下面全部要求的两个整数&#xff1a; 两数乘积等于 num 1 或 num 2以绝对差进行度量&#xff0c;两数大小最接近 你可以按任意顺序返回这两个整数。 示例 1&#xff1a; 输入&#xff1a;num 8 输出&#xff1a;[3…

光子神经网络登上nature,图像识别速度降至1纳秒

文 | Alex&#xff08;凹非寺&#xff09;源 | 量子位比深度神经网络速度还快的是什么&#xff1f;或许光子DNN可以回答这个问题。现在&#xff0c;美国研究者开发的一个光子神经网络(photonic deep neural network&#xff0c;PDNN)&#xff0c;让图像识别仅需1纳秒。1纳秒是什…

LeetCode 1363. 形成三的最大倍数(贪心,难)

1. 题目 给你一个整数数组 digits&#xff0c;你可以通过按任意顺序连接其中某些数字来形成 3 的倍数&#xff0c;请你返回所能得到的最大的 3 的倍数。 由于答案可能不在整数数据类型范围内&#xff0c;请以字符串形式返回答案。 如果无法得到答案&#xff0c;请返回一个空…

Spring Boot中使用Spring Security进行安全控制

我们在编写Web应用时&#xff0c;经常需要对页面做一些安全控制&#xff0c;比如&#xff1a;对于没有访问权限的用户需要转到登录表单页面。要实现访问控制的方法多种多样&#xff0c;可以通过Aop、拦截器实现&#xff0c;也可以通过框架实现&#xff08;如&#xff1a;Apache…