Concept and application of Spring Statemachine

Posted by echoindia756 on Thu, 18 Jun 2020 05:48:33 +0200

From https://www.jianshu.com/p/9ee887e045dd , thank you

1 Finite-state machine

1.1 definition of state machine

FSM (finite state machine, FSM), also known as FSM, is a mathematical model that represents finite states, their transitions and actions.

Finite state machine embodies two points: first discrete, then finite.

  • State: the word state is somewhat difficult to define. State stores information about the past, which means it reflects the input changes from the beginning of the system to the present.
  • Actions & transitions: transitions indicate state changes and are described by conditions that must be met to ensure that the transition occurs. An action is a description of the activity to be performed at a given time.
  • Guards: the reason why the detector appears is to detect whether the conditions for switching from one state to another are met.
  • Event: event, see also event. Generally speaking, something important to the system is called event.

1.2 state machine example

Real world example: ticket gate, from wiki

The revolving door used to control the subway and amusement park facilities is a door, with three revolving arms at the waist height and a passage across the entrance. Initially, the arm was locked, blocking the entrance and preventing customers from passing through. Storing coins or tokens in a slot on the revolving door unlocks the arm and allows a single customer to pass through. After the customer passes, lock the arm again until another coin is inserted.

The revolving door is regarded as a state machine, and there are two possible states: locking and unlocking. There are two inputs that can affect its status: place the coin in the slot (coin) and push the arm (push). In the Locked state, pushing the arm is invalid; it is Locked no matter how many times it is pushed. Input coins - that is, input coins to the machine - to change the status from Locked to unlocked. In the unlocked state, adding extra coins is invalid; that is, giving extra coins input does not change the state. However, the customer pushes the arm, performs push input, and turns the state back to Locked.

The revolving door state machine can be represented by the state conversion table, displaying each possible state, the conversion between them (based on the input given to the machine) and the output generated by each input:

Current State Input Next State Output
Locked coin Unlocked Unlock the revolving door so that visitors can pass
Locked push Locked None
Unlocked coin Unlocked None
Unlocked push Locked When visitors pass, lock the revolving door

The rotating gate state machine can also be represented by a directed graph called a state graph (above). Each state is represented by a node (circle). The edge (arrow) shows the transition from one state to another. Each arrow is marked with the input that triggers the transition. Inputs that do not cause a state change (such as coin inputs that are unlocked) are represented by circular arrows that return to the original state. The arrow entering the Locked node from the black dot indicates that it is the initial state.

state diagram

2 Spring Statemachine

2.1 positioning and characteristics

Spring state machine is a framework for application developers to use state machine concepts with spring applications.

Spring Statemachine provides the following features:

  • Easy to use flat one level state machine for simple use cases
  • Hierarchical state machine structure to ease complex state configuration
  • State machine regions to provide even more complex state configurations
  • Usage of triggers, transitions, guards and actions
  • Type safe configuration adapter
  • Builder pattern for easy instance for use outside of Spring Application context
  • Recipes for usual use cases
  • Distributed state machine based on a Zookeeper State machine event listeners
  • UML Eclipse Papyrus modeling
  • Store machine config in a persistent storage
  • Spring IOC integration to associate bean s with a state machine

2.2 development and community

Related resources:

  • Spring Statemachine home page: Home
  • Spring Statemachine Github: Github,Star: 500+ & Fork: 200+ (201809)
  • Spring Statemachine Gitter: Gitter, less than 100 people

Release:

Latest version: v2.0.2.RELEASE And v1.2.12.RELEASE It has been released 44 times. Please check Github homepage for more details and latest information.

3 function example

3.1 basic functions

Continue to rotate the real example of the door and add Maven dependency. The version used in this example is as follows:

 

<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-core</artifactId>
    <version>2.0.2.RELEASE</version>
</dependency>

Define the state of revolving door: lock, unlock, use enumeration

 

public enum TurnstileStates {
    Unlocked, Locked
}

Define revolving door operation event: push door and coin, use enumeration

 

public enum TurnstileEvents {
    COIN, PUSH
}

State machine configuration, in which turnstileUnlock() and customerPassAndLock() are the extended business operations after the current state change, which can be modified according to the actual business scenario

 

@Configuration
@EnableStateMachine
public class StatemachineConfigurer extends EnumStateMachineConfigurerAdapter<TurnstileStates, TurnstileEvents> {

    @Override
    public void configure(StateMachineStateConfigurer<TurnstileStates, TurnstileEvents> states)
            throws Exception {
        states
                .withStates()
                // Initial state: Locked
                .initial(TurnstileStates.Locked)
                .states(EnumSet.allOf(TurnstileStates.class));
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<TurnstileStates, TurnstileEvents> transitions)
            throws Exception {
        transitions
                .withExternal()
                .source(TurnstileStates.Unlocked).target(TurnstileStates.Locked)
                .event(TurnstileEvents.COIN).action(customerPassAndLock())
                .and()
                .withExternal()
                .source(TurnstileStates.Locked).target(TurnstileStates.Unlocked)
                .event(TurnstileEvents.PUSH).action(turnstileUnlock())
        ;
    }

    @Override
    public void configure(StateMachineConfigurationConfigurer<TurnstileStates, TurnstileEvents> config)
            throws Exception {
        config.withConfiguration()
                .machineId("turnstileStateMachine")
        ;
    }

    public Action<TurnstileStates, TurnstileEvents> turnstileUnlock() {
        return context -> System.out.println("Unlock the revolving door so that visitors can pass" );
    }

    public Action<TurnstileStates, TurnstileEvents> customerPassAndLock() {
        return context -> System.out.println("When visitors pass, lock the revolving door" );
    }

}

Startup classes and test cases

 

@SpringBootApplication
public class StatemachineApplication implements CommandLineRunner {

    @Autowired
    private StateMachine<TurnstileStates, TurnstileEvents> stateMachine;

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

    @Override
    public void run(String... strings) throws Exception {
        stateMachine.start();
        System.out.println("--- coin ---");
        stateMachine.sendEvent(TurnstileEvents.COIN);
        System.out.println("--- coin ---");
        stateMachine.sendEvent(TurnstileEvents.COIN);
        System.out.println("--- push ---");
        stateMachine.sendEvent(TurnstileEvents.PUSH);
        System.out.println("--- push ---");
        stateMachine.sendEvent(TurnstileEvents.PUSH);
        stateMachine.stop();
    }
}

The result output is consistent with that described by the state machine in the morning.

 

--- push ---
Unlock the revolving door so that visitors can pass
--- push ---
--- coin ---
When visitors pass, lock the revolving door
--- coin ---

3.2 practical functions

3.2.1 state storage

State machine persistence. In the actual environment, the current state is often obtained in real time from the persistence media. Spring Statemachine implements StateMachinePersist interface, write and read the state of the current state machine

In this case, HashMap is used as the simulation storage medium, and the real state acquisition method is needed in the formal project

 

@Component
public class BizStateMachinePersist implements StateMachinePersist<TurnstileStates, TurnstileEvents, Integer> {

    static Map<Integer, TurnstileStates> cache = new HashMap<>(16);

    @Override
    public void write(StateMachineContext<TurnstileStates, TurnstileEvents> stateMachineContext, Integer integer) throws Exception {
        cache.put(integer, stateMachineContext.getState());
    }

    @Override
    public StateMachineContext<TurnstileStates, TurnstileEvents> read(Integer integer) throws Exception {
        // Note that the initial state of the state machine is consistent with that defined in the configuration
        return cache.containsKey(integer) ?
                new DefaultStateMachineContext<>(cache.get(integer), null, null, null, null, "turnstileStateMachine") :
                new DefaultStateMachineContext<>(TurnstileStates.Locked, null, null, null, null, "turnstileStateMachine");
    }
}

Published in StatemachineConfigurer

 

@Autowired
private BizStateMachinePersist bizStateMachinePersist;

@Bean
public StateMachinePersister<TurnstileStates, TurnstileEvents, Integer> stateMachinePersist() {
    return new DefaultStateMachinePersister<>(bizStateMachinePersist);
}

Used in StatemachineApplication to automatically inject StateMachinePersister object. The test case is as follows

 

stateMachine.start();

stateMachinePersist.restore(stateMachine, 1);
System.out.println("--- push ---");
stateMachine.sendEvent(TurnstileEvents.PUSH);
stateMachinePersist.persist(stateMachine, 1);

stateMachinePersist.restore(stateMachine, 1);
System.out.println("--- push ---");
stateMachine.sendEvent(TurnstileEvents.PUSH);
stateMachinePersist.persist(stateMachine, 1);

stateMachinePersist.restore(stateMachine, 1);
System.out.println("--- coin ---");
stateMachine.sendEvent(TurnstileEvents.COIN);
stateMachinePersist.persist(stateMachine, 1);

stateMachinePersist.restore(stateMachine, 1);
System.out.println("--- coin ---");
stateMachine.sendEvent(TurnstileEvents.COIN);
stateMachinePersist.persist(stateMachine, 1);

stateMachine.stop();

3.2.2 action monitoring

Define action monitoring class, StatemachineMonitor (name optional), add annotation @ WithStateMachine. In this example, id is used to bind the state machine. According to the document definition, name and id can be used to bind the state machine instance to be monitored. If no name or id is defined, the default listening name is stateMachine.

 

@WithStateMachine(id = "turnstileStateMachine")
public class StatemachineMonitor {

    @OnTransition
    public void anyTransition() {
        System.out.println("--- OnTransition --- init");
    }

    @OnTransition(target = "Unlocked")
    public void toState1() {
        System.out.println("--- OnTransition --- toState1");
    }

    @OnStateChanged(source = "Unlocked")
    public void fromState1() {
        System.out.println("--- OnTransition --- fromState1");
    }
}

Event monitoring of other contexts, described in subsequent articles, Official website link

3.2.2 state machine engineering

In the actual business environment, multithreading is often used to process the state corresponding to different business IDs. In the state machine, using the context of events to transfer data will cause multithreading problems. It is necessary to use the state machine engineering to create different state machines with UUID.

In the StatemachineConfigurer class, change @ EnableStateMachine to @ EnableStateMachineFactory, and add state machine processing action encapsulation method. Readers can customize according to business scenarios. This example is a feasible solution

 

@Service
public class StatemachineService {

    @Autowired
    private StateMachinePersister<TurnstileStates, TurnstileEvents, Integer> stateMachinePersist;
    @Autowired
    private StateMachineFactory<TurnstileStates, TurnstileEvents> stateMachineFactory;

    public void execute(Integer businessId, TurnstileEvents event, Map<String, Object> context) {
        // The state machine is created with the tag ID, which is not bound to the specific definition state machine
        StateMachine<TurnstileStates, TurnstileEvents> stateMachine = stateMachineFactory.getStateMachine(UUID.randomUUID());
        stateMachine.start();
        try {
            // During restore of BizStateMachinePersist, bind turnstileStateMachine state machine to listen for related events
            stateMachinePersist.restore(stateMachine, businessId);
            // This method is tedious. In fact, map < string and Object > context are injected into message
            MessageBuilder<TurnstileEvents> messageBuilder = MessageBuilder
                    .withPayload(event)
                    .setHeader("BusinessId", businessId);
            if (context != null) {
                context.entrySet().forEach(p -> messageBuilder.setHeader(p.getKey(), p.getValue()));
            }
            Message<TurnstileEvents> message = messageBuilder.build();

            // Send event to return whether the execution is successful
            boolean success = stateMachine.sendEvent(message);
            if (success) {
                stateMachinePersist.persist(stateMachine, businessId);
            } else {
                System.out.println("State machine processing did not succeed. Please process, ID: " + businessId + ",current context: " + context);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            stateMachine.stop();
        }
    }
}

For a complete example, see: link

4 reference examples

Topics: Spring github Zookeeper Eclipse