天天看點

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的判斷,使得代碼顯得更為簡潔,擁有了更好的可讀性。