天天看点

Spring StateMachine学习(一) ——快速开始

有限状态机(Finite-state machine)

有限状态机(英语:finite-state machine,缩写:FSM),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。应用FSM模型可以帮助对象生命周期的状态的顺序以及导致状态变化的事件进行管理。将状态和事件控制从不同的业务Service方法的if else中抽离出来。FSM的应用范围很广,对于有复杂状态流,扩展性要求比较高的场景都可以使用该模型。下面是状态机模型中的4个要素,即现态、条件、动作、次态。

  • 现态:是指当前所处的状态。
  • 条件:又称为“事件”。当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。
  • 动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。
  • 次态:条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了。

Spring-statemachine 快速开始

Spring StateMachine框架可能对于大部分使用Spring的开发者来说还比较生僻。它的主要功能是帮助开发者简化状态机的开发过程,让状态机结构更加层次化。

我们通过一个简单的示例来对Spring StateMachine有一个初步的认识。假设当前有一个审批流程,包含审批通过、审批拒绝、审批超时拒绝这三个状态。

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

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

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.mrfresh.stateMachine</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.statemachine/spring-statemachine-core -->
        <dependency>
            <groupId>org.springframework.statemachine</groupId>
            <artifactId>spring-statemachine-core</artifactId>
            <version>2.2.0.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.2</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- 构建Restful API -->
        <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

           

2、根据上面所述的订单需求场景定义状态和事件枚举,具体如下:

@AllArgsConstructor
public enum RefundReasonStatus implements IEnum<Integer> {
    READY_TO_APPROVE(1, "待审批"),
    APPROVED(2, "审核通过"),
    REJECT(3, "审核拒绝"),
    AUTO_REJECT(4, "超时自动拒绝"),
    ;
    @EnumValue
    private final int value;
    @Getter
    private final String msg;
    public static RefundReasonStatus valueOf(int val) {
        Optional<RefundReasonStatus> optional = Arrays.stream(values())
                .filter(instance -> instance.value == val).limit(1)
                .findFirst();
        if (optional.isPresent()) {
            return optional.get();
        }
        throw new IllegalArgumentException("未知退款理由审批状态类型: " + val);
    }
    @Override
    public Integer getValue() {
        return value;
    }
}

           
@AllArgsConstructor
public enum RefundReasonEvents implements IEnum<Integer> {
    APPROVE(1, "审核通过"),
    REJECT(2, "审批拒绝"),
    TIME_OUT(3, "审批超时"),
    ;
    @EnumValue
    private final int value;
    @Getter
    private final String msg;
    public static RefundReasonEvents valueOf(int val) {
        Optional<RefundReasonEvents> optional = Arrays.stream(values())
                .filter(instance -> instance.value == val).limit(1)
                .findFirst();
        if (optional.isPresent()) {
            return optional.get();
        }
        throw new IllegalArgumentException("未知退款理由审批事件类型: " + val);
    }
    @Override
    public Integer getValue() {
        return value;
    }
}
           

3、创建状态机配置类:

@Slf4j
@Configuration
@EnableStateMachine
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<RefundReasonStatus, RefundReasonEvents> {

    /**
     * @Author xyhua
     * @Description 用来初始化当前状态机拥有哪些状态
     * @Date 16:54 2020-04-13
     * @param
     * @return
     **/
    @Override
    public void configure(StateMachineStateConfigurer<RefundReasonStatus, RefundReasonEvents> states)
            throws Exception {
        states.withStates()
                .initial(RefundReasonStatus.READY_TO_APPROVE)
                .states(EnumSet.allOf(RefundReasonStatus.class));
    }

    /**
     * @Author xyhua
     * @Description 初始化当前状态机有哪些状态迁移动作,其中命名中我们很容易理解每一个迁移动作
     * @Date 18:29 2020-04-13
     * @param 
     * @return 
     **/
    @Override
    public void configure(StateMachineTransitionConfigurer<RefundReasonStatus, RefundReasonEvents> transitions)
            throws Exception {
        transitions
                .withExternal()
                .source(RefundReasonStatus.READY_TO_APPROVE).target(RefundReasonStatus.APPROVED)
                .event(RefundReasonEvents.APPROVE)
                .and()
                .withExternal()
                .source(RefundReasonStatus.READY_TO_APPROVE).target(RefundReasonStatus.REJECT)
                .event(RefundReasonEvents.REJECT)
                .and()
                .withExternal()
                .source(RefundReasonStatus.READY_TO_APPROVE).target(RefundReasonStatus.AUTO_REJECT)
                .event(RefundReasonEvents.TIME_OUT);
    }

    /**
     * @Author xyhua
     * @Description 指定状态机的处理监听器
     * @Date 18:29 2020-04-13
     * @param 
     * @return 
     **/
    @Override
    public void configure(StateMachineConfigurationConfigurer<RefundReasonStatus, RefundReasonEvents> config)
            throws Exception {
        config
                .withConfiguration()
                .listener(listener());
    }

    /**
     * @Author xyhua
     * @Description 状态机监听器
     * @Date 18:30 2020-04-13
     * @param 
     * @return 
     **/
    @Bean
    public StateMachineListener<RefundReasonStatus, RefundReasonEvents> listener() {
        return new StateMachineListenerAdapter<RefundReasonStatus, RefundReasonEvents>() {
            @Override
            public void transition(Transition<RefundReasonStatus, RefundReasonEvents> transition) {
                if(transition.getTarget().getId() == RefundReasonStatus.READY_TO_APPROVE) {
                    log.info("待审批");
                    return;
                }

                if(transition.getSource().getId() == RefundReasonStatus.READY_TO_APPROVE
                        && transition.getTarget().getId() == RefundReasonStatus.APPROVED) {
                    log.info("审批通过");
                    return;
                }

                if(transition.getSource().getId() == RefundReasonStatus.READY_TO_APPROVE
                        && transition.getTarget().getId() == RefundReasonStatus.REJECT) {
                    log.info("审批拒绝");
                    return;
                }

                if(transition.getSource().getId() == RefundReasonStatus.READY_TO_APPROVE
                        && transition.getTarget().getId() == RefundReasonStatus.AUTO_REJECT) {
                    log.info("自动审批拒绝");
                    return;
                }
            }
        };
    }
}
           

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

  • @EnableStateMachine

    注解用来启用

    Spring StateMachine

    状态机功能
  • configure(StateMachineStateConfigurer<States, Events> states)

    方法用来初始化当前状态机拥有哪些状态,其中

    initial(RefundReasonStatus.READY_TO_APPROVE)

    定义了初始状态为

    READY_TO_APPROVE

    states(EnumSet.allOf(RefundReasonStatus.class))

    则指定了使用上一步中定义的所有状态作为该状态机的状态定义。
  • configure(StateMachineTransitionConfigurer<States, Events> transitions)

    方法用来初始化当前状态机有哪些状态迁移动作,其中命名中我们很容易理解每一个迁移动作,都有来源状态

    source

    ,目标状态

    target

    以及触发事件

    event

  • configure(StateMachineConfigurationConfigurer<States, Events> config)

    方法为当前的状态机指定了状态监听器,其中

    listener()

    则是调用了下一个内容创建的监听器实例,用来处理各个各个发生的状态迁移事件。
  • StateMachineListener<States, Events> listener()

    方法用来创建

    StateMachineListener

    状态监听器的实例,在该实例中会定义具体的状态迁移处理逻辑,上面的实现中只是做了一些输出,实际业务场景会会有更负责的逻辑,所以通常情况下,我们可以将该实例的定义放到独立的类定义中,并用注入的方式加载进来。

4、测试类

@RestController
public class TestController {
    @Autowired
    private StateMachine<RefundReasonStatus, RefundReasonEvents> stateMachine;


    @GetMapping("test/machine")
    public void testMachine() {
        stateMachine.start();
        stateMachine.sendEvent(RefundReasonEvents.APPROVE);
    }
}
           

优化状态监听

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

public interface StateMachineListener<S, E> {
    void stateChanged(State<S, E> var1, State<S, E> var2);

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

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

    void eventNotAccepted(Message<E> var1);

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

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

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

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

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

    void stateMachineError(StateMachine<S, E> var1, Exception var2);

    void extendedStateChanged(Object var1, Object var2);

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

注解监听器

对于状态监听器,Spring StateMachine还提供了优雅的注解配置实现方式,所有

StateMachineListener

接口中定义的事件都能通过注解的方式来进行配置实现。比如,我们可以将之前实现的状态监听器用注解配置来做进一步的简化:

@Slf4j
@WithStateMachine
public class EventListener {

    @OnTransition(target = "READY_TO_APPROVE")
    public void readyToApprove() {
        log.info("待审批");
    }

    @OnTransition(target = "APPROVED")
    public void approve() {
        log.info("审批通过");
    }

    @OnTransition(target = "REJECT")
    public void reject() {
        log.info("审批拒绝");
    }

    @OnTransition(target = "AUTO_REJECT")
    public void autoReject() {
        log.info("自动审批拒绝");
    }
}
           

上述代码实现了与快速入门中定义的

listener()

方法创建的监听器相同的功能,但是由于通过注解的方式配置,省去了原来事件监听器中各种if的判断,使得代码显得更为简洁,拥有了更好的可读性。