Spring Boot 2.x Actual StateMachine

This article was first published on a personal website: Spring Boot 2.x Actual StateMachine

Spring StateMachine is a state machine framework. In the Spring Framework project, developers can obtain a business state machine by simple configuration without having to manage the process of defining, initializing and so on.In this article today, we'll look at the use of the Spring StateMachine framework through a case study.

Case introduction

Suppose that in a business system, there is an object with three states: draft, to be published, to be published, and to be published. Business actions for these three states are also simple: online, publishing, and rollback.The business state machine is illustrated below.

actual combat

Next, the Spring StateMachine is presented based on the above business state machine.

  • Create a basic Spring Boot project that adds Spring StateMachine dependencies to the main pom file:
<?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.1.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>online.javaadu</groupId>
  <artifactId>statemachinedemo</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>statemachinedemo</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</artifactId>
    </dependency>

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </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>

    <!--join spring statemachine Dependency on-->
        <dependency>
        <groupId>org.springframework.statemachine</groupId>
        <artifactId>spring-statemachine-core</artifactId>
        <version>2.1.3.RELEASE</version>
      </dependency>
  </dependencies>

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

</project>

Define state and event enumerations with the following code:

/**
* State Enumeration
**/
public enum States {
    DRAFT,
    PUBLISH_TODO,
    PUBLISH_DONE,
}

/**
* Event Enumeration
**/
public enum Events {
    ONLINE,
    PUBLISH,
    ROLLBACK
}
  • Complete the configuration of the state machine, including: (1) the initial state and all States of the state machine; (2) the transition rules between states
@Configuration
@EnableStateMachine
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<States, Events> {

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

    @Override
    public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception {
        transitions.withExternal()
            .source(States.DRAFT).target(States.PUBLISH_TODO)
            .event(Events.ONLINE)
            .and()
            .withExternal()
            .source(States.PUBLISH_TODO).target(States.PUBLISH_DONE)
            .event(Events.PUBLISH)
            .and()
            .withExternal()
            .source(States.PUBLISH_DONE).target(States.DRAFT)
            .event(Events.ROLLBACK);
    }
}
  • Define a test business object whose state machine state transition is reflected in the state change of the business object
@WithStateMachine
@Data
@Slf4j
public class BizBean {

    /**
     * @see States
     */
    private String status = States.DRAFT.name();

    @OnTransition(target = "PUBLISH_TODO")
    public void online() {
        log.info("Operation online, to be published. target status:{}", States.PUBLISH_TODO.name());
        setStatus(States.PUBLISH_TODO.name());
    }

    @OnTransition(target = "PUBLISH_DONE")
    public void publish() {
        log.info("Operation Publishing,Publishing complete. target status:{}", States.PUBLISH_DONE.name());
        setStatus(States.PUBLISH_DONE.name());
    }

    @OnTransition(target = "DRAFT")
    public void rollback() {
        log.info("Operation rollback,Return to Draft. target status:{}", States.DRAFT.name());
        setStatus(States.DRAFT.name());
    }

}
  • Writing a test case, instead of using the CommandLineRunner interface, we define a StartupRunner in which a state machine is started, different events are sent, and the state machine's flow is verified by a log.
public class StartupRunner implements CommandLineRunner {

    @Resource
    StateMachine<States, Events> stateMachine;

    @Override
    public void run(String... args) throws Exception {
        stateMachine.start();
        stateMachine.sendEvent(Events.ONLINE);
        stateMachine.sendEvent(Events.PUBLISH);
        stateMachine.sendEvent(Events.ROLLBACK);
    }
}

After running the above program, we can get the following output in the console. We do three things: go online, publish, rollback, and we do see the corresponding log in the figure below.However, I also found one unexpected thing - when I started the state machine, I also printed a log - "Operation rollback, back to draft state. target status:DRAFT", which should have been triggered when the state machine set its initial state.

Analysis

The steps to use the Spring StateMachine are as follows, as shown in the above battle process:

  1. Define state and event enumerations
  2. Define the initial state and all States of the state machine
  3. Define transition rules between states
  4. Use state machines in business objects to write listener methods that respond to state changes

In order to unify the management of state change operations, we will consider introducing state machines into the project so that other business modules are isolated from the state transfer module, and other business modules are not obsessed with what the current state is and what they should do.When applying a state machine to fulfill business requirements, the key is to analyze the business state. As long as the state machine is designed properly, the specific implementation can either choose to use Spring StateMachine or implement a state machine by itself.

The advantage of using Spring StateMachine is that you don't need to care about the implementation details of the state machine, you just need to care about the state of your business, what are the transition rules between them, and what business operations you really want to perform after each state transition.

For a complete example of this article, see: https://github.com/duqicauc/Spring-Boot-2.x-In-Action/tree/master/statemachinedemo

Reference material

  1. http://blog.didispace.com/spring-statemachine/
  2. https://projects.spring.io/spring-statemachine/#quick-start

This number focuses on topics such as back-end technology, JVM problem solving and optimization, Java interview questions, personal growth and self-management, providing readers with first-line developer work and growth experience, and we look forward to seeing what you can do here.

Tags: Java Spring Maven Apache

Posted on Sun, 10 Nov 2019 14:57:57 -0500 by simon13