Moment For Technology

? [SpringBoot technology special topic] "StateMachine" StateMachine design and implementation

Posted on June 26, 2022, 7:37 p.m. by 刁冠宇
Category: The back-end Tag: The back-end Spring Boot

This is the 12th day of my participation in Gwen Challenge

Preface introduces

This article introduces state machines and related concepts. Examples of how to integrate Spring-Statemachine in Springboot with a simple order status process.

Finite-state machine (FINite-State Machine)

A finite-state machine (FSM), or FSM for short, is a mathematical model that represents a finite number of states and the transfer and action between these states.

Applying the FSM model helps manage the order of states in the object life cycle and the events that lead to state changes. Separate state and event control from the if else of the different business Service methods. FSM can be used in a wide range of scenarios with complex state flows and high scalability requirements.

The following are the four elements of the state machine model, namely, appearance, condition, action, and secondary state.

  • State: refers to the current state.
  • Conditions: also known as "events". When a condition is met, an action is triggered or a state migration is performed.
  • Action: The action performed after the condition is met. After the action is completed, it can be migrated to a new state, or it can remain in the original state. The action is not required. If the condition is met, you can migrate to the new state without performing any action.
  • Secondary state: a new state to which a condition is satisfied. The "secondary state" is relative to the "state", once the "secondary state" is activated, it is transformed into a new "state".

In a state machine, each state has a corresponding behavior, and the state is switched as the behavior is triggered. One approach is to implement a state machine mechanism using two-dimensional arrays, where the abscissa represents behavior, the ordinate represents state, and specific values represent the current state.

Let's design a state machine for the login scenario.

Design a state machine table.

The horizontal axis is action and the vertical axis is state

Now its two-dimensional array looks like this

  • In addition, we can implement a state machine through the state pattern, which encapsulates each state into a separate class whose behavior changes with the internal state. The state mode uses classes to represent state, so we can easily change the state of an object by switching classes, avoiding long conditional branching statements,
  • So that the system has better flexibility and scalability. Now let's define an enumeration of states, including unconnected, connected, registered, and registered.

Define an environment class that is the object that actually owns the state.

The state mode represents state by class, which makes it easy to change the state of an object by switching classes. We define several state classes.

Note that if an action does not trigger a state change, we can throw a RuntimeException. In addition, when called, state switching is controlled through the environment class, as shown below.

Spring StateMachine makes the StateMachine structure more hierarchical, helping developers simplify the development process of state machines. Now, let's do the transformation with Spring StateMachine. Modify the POM file to add Maven/gradle dependencies.

Dependencies {the compile 'org. Springframework. The statemachine: spring - the statemachine - core: 1.2.7. RELEASE'}Copy the code

Define an enumeration of states including unconnected, Connected, Registered, and registered.

public enum RegStatusEnum {
    / / not connected
    UNCONNECTED,
    / / connected
    CONNECTED,
    // Logging in
    LOGINING,
    // Log in to system
    LOGIN_INTO_SYSTEM;
}
Copy the code

Define an enumeration of events whose occurrence triggers a state transition

public enum RegEventEnum {
    / / the connection
    CONNECT,
    // Start logging in
    BEGIN_TO_LOGIN,
    // Login succeeded
    LOGIN_SUCCESS,
    // Login failed
    LOGIN_FAILURE,
    // Log out
    LOGOUT;
}
Copy the code

Configure the state machine and enable the state machine function through annotations. Configuration should inherit EnumStateMachineConfigurerAdapter commonly, and rewrite some initial state to configure method to configure the state machine as well as between event and state transition.

import static com.qyz.dp.state.events.RegEventEnum.BEGIN_TO_LOGIN;
import static com.qyz.dp.state.events.RegEventEnum.CONNECT;
import static com.qyz.dp.state.events.RegEventEnum.LOGIN_FAILURE;
import static com.qyz.dp.state.events.RegEventEnum.LOGIN_SUCCESS;
import static com.qyz.dp.state.events.RegEventEnum.LOGOUT;
import static com.qyz.dp.state.state.RegStatusEnum.CONNECTED;
import static com.qyz.dp.state.state.RegStatusEnum.LOGINING;
import static com.qyz.dp.state.state.RegStatusEnum.LOGIN_INTO_SYSTEM;
import static com.qyz.dp.state.state.RegStatusEnum.UNCONNECTED;

import java.util.EnumSet;

import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;

import com.qyz.dp.state.events.RegEventEnum;
import com.qyz.dp.state.state.RegStatusEnum;

@Configuration
@EnableStateMachine // Enable state machine configuration
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter{

    /** * Configures the state machine state */
    @Override
    public void configure(StateMachineStateConfigurer states) throws Exception {
        states.withStates()
        // Initialize the state machine state
        .initial(RegStatusEnum.UNCONNECTED)
        // Specify all states of the state machine
        .states(EnumSet.allOf(RegStatusEnum.class));
    }

    /** * Configure the state machine state transition */
    @Override
    public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
        // 1. connect UNCONNECTED -gt; CONNECTED
        transitions.withExternal()
            .source(UNCONNECTED)
            .target(CONNECTED)
            .event(CONNECT)
        // 2. beginToLogin CONNECTED -gt; LOGINING
        .and().withExternal()
            .source(CONNECTED)
            .target(LOGINING)
            .event(BEGIN_TO_LOGIN)
        // 3. login failure LOGINING -gt; UNCONNECTED
        .and().withExternal()
            .source(LOGINING)
            .target(UNCONNECTED)
            .event(LOGIN_FAILURE)
        // 4. login success LOGINING -gt; LOGIN_INTO_SYSTEM
        .and().withExternal()
            .source(LOGINING)
            .target(LOGIN_INTO_SYSTEM)
            .event(LOGIN_SUCCESS)
        // 5. logout LOGIN_INTO_SYSTEM -gt; UNCONNECTED.and().withExternal() .source(LOGIN_INTO_SYSTEM) .target(UNCONNECTED) .event(LOGOUT); }}Copy the code

Spring StateMachine provides an annotation configuration implementation. All events defined in the StateMachineListener interface can be configured using annotations. In the case of a connection event, the @onTransition source specifies the original state, the target specifies the target state, and when the event is triggered it will be listened on to call the connect() method.

When starting SpringBoot, you need to inject the state of the state machine and the configuration of events. It mainly involves the following two classes:

  • StateMachineStateConfigurer S, E configuration state collection and initial state, the generic parameter S on behalf of the state, E for events.

  • StateMachineTransitionConfigurer flow configuration state transfer, can define the state transition event.

Configure event listeners, actions that are triggered when an event occurs

import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.annotation.OnTransition;
import org.springframework.statemachine.annotation.WithStateMachine;

@Configuration
@WithStateMachine
public class StateMachineEventConfig {

    @OnTransition(source = "UNCONNECTED", target = "CONNECTED")
    public void connect(a) {
        System.out.println("Switch state from UNCONNECTED to CONNECTED: connect");
    }

    @OnTransition(source = "CONNECTED", target = "LOGINING")
    public void beginToLogin(a) {
        System.out.println("Switch state from CONNECTED to LOGINING: beginToLogin");
    }

    @OnTransition(source = "LOGINING", target = "LOGIN_INTO_SYSTEM")
    public void loginSuccess(a) {
        System.out.println("Switch state from LOGINING to LOGIN_INTO_SYSTEM: loginSuccess");
    }

    @OnTransition(source = "LOGINING", target = "UNCONNECTED")
    public void loginFailure(a) {
        System.out.println("Switch state from LOGINING to UNCONNECTED: loginFailure");      
    }
    
    @OnTransition(source = "LOGIN_INTO_SYSTEM", target = "UNCONNECTED")
    public void logout(a)
    {
        System.out.println("Switch state from LOGIN_INTO_SYSTEM to UNCONNECTED: logout"); }}Copy the code

A state machine is automatically assembled through annotations and a REST interface is written here to trigger state machine changes


@RestController
public class WebApi {

    @Autowired
    private StateMachine stateMachine;
    
    @GetMapping(value = "/testStateMachine")
    public void testStateMachine(a)
    {
        stateMachine.start();
        stateMachine.sendEvent(RegEventEnum.CONNECT);
        stateMachine.sendEvent(RegEventEnum.BEGIN_TO_LOGIN);
        stateMachine.sendEvent(RegEventEnum.LOGIN_FAILURE);
        stateMachine.sendEvent(RegEventEnum.LOGOUT);
    }
}
Switch state from UNCONNECTED to CONNECTED: connect
Switch state from CONNECTED to LOGINING: beginToLogin
Switch state from LOGINING to UNCONNECTED: loginFailure
Copy the code
  • As you can see from the output, although four events were sent, only three were sent. The reason is that when the last LOGOUT event occurs, the state machine is in UNCONNECTED state and there is no state transfer associated with the LOGOUT event. Therefore, no operation is performed.
  • The state machine implemented by Spring has all the relationships between classes managed by the IOC container, achieving true decoupling. Sure enough, Spring method is good.

Spring StateMachine makes the StateMachine structure more hierarchical, so let's review a few core steps:

  • The first step is to define the state enumeration.

  • Second, define the event enumeration.

  • Third, define the state machine configuration, set the initial state, and the relationship between the state and the event.

  • Fourth, define a state listener that triggers a method when the state changes.


Listeners for state transitions

During state transition, listeners can be used to handle tasks such as persistence or service monitoring. In scenarios where persistence is required, you can add the handling of persistence to listeners in the state machine mode.

Which mainly involves

StateMachineListener event listener (implemented through Spring's Event mechanism).

  • Listen for stateEntered, stateExited, eventNotAccepted, transition, transitionStarted, transitionEnded StateMachineStarted (state machine startup), stateMachineStopped(state shutdown), stateMachineError(state machine exception), and other events can be traced by using the listener.

  • StateChangeInterceptor Interceptor interface, which is different from a Listener. It can change the state transition chain. PreEvent (event preprocessing), preStateChange(state change preprocessing), postStateChange(state change postprocessing), preTransition(transformation preprocessing), postTransition(transformation postprocessing), S Execution points such as tateMachineError(exception handling) take effect.

  • StateMachine StateMachine instance. Spring StateMachine supports singleton and factory mode creation. Each StateMachine has a unique machineId for identifying machine instances. Note that the statemachine instance stores context-specific properties such as the current statemachine internally, and therefore cannot be shared by multiple threads.

To make it easier to expand Listeners and manage Listeners and Interceptors. You can define a Handler: based on the state machine instance PersistStateMachineHandler, and listeners OrderPersistStateChangeListener persistent entities are as follows:

Listener Handler and interface definition PersistStateMachineHandler:

public class PersistStateMachineHandler extends LifecycleObjectSupport {

   private final StateMachineOrderStatus, OrderStatusChangeEvent stateMachine;
   private final PersistingStateChangeInterceptor interceptor = new 
         PersistingStateChangeInterceptor();
   private final CompositePersistStateChangeListener listeners = new 
         CompositePersistStateChangeListener();

   /** * instantiate a new persistent state machine Handler **@paramStateMachine stateMachine instance */
   public PersistStateMachineHandler(StateMachine
       
         stateMachine)
       , {
       Assert.notNull(stateMachine, "State machine must be set");
       this.stateMachine = stateMachine;
   }

   @Override
   protected void onInit(a) throws Exception { stateMachine.getStateMachineAccessor().doWithAllRegions(function - function.addStateMachineInterceptor(interceptor));  }/** * Handle the entity event **@param event
    * @param state
    * @returnReturns true */ if the event is accepted for processing
   public boolean handleEventWithState(Message
       
         event, OrderStatus state)
        {
       stateMachine.stop();
       ListStateMachineAccessOrderStatus, OrderStatusChangeEvent withAllRegions = 
        stateMachine.getStateMachineAccessor()
               .withAllRegions();
       for (StateMachineAccessOrderStatus, OrderStatusChangeEvent a : withAllRegions) {
           a.resetStateMachine(new DefaultStateMachineContext(state, null.null.null));
       }
       stateMachine.start();
       return stateMachine.sendEvent(event);
   }

   /** * Add listener **@param listener the listener
    */
   public void addPersistStateChangeListener(PersistStateChangeListener listener) {
       listeners.register(listener);
   }


   / * * * through addPersistStateChangeListener, can increase the current Handler PersistStateChangeListener. * on the persistence of state change triggered, invokes the corresponding realized PersistStateChangeListener Listener instance. * /
   public interface PersistStateChangeListener {

       /** * This method is called when the state is persisted@param state
        * @param message
        * @param transition
        * @paramStateMachine stateMachine instance */
       void onPersist(State
       
         state, Message
        
          message, Transition
         
           transition, StateMachine
          
            stateMachine)
          ,
         ,
        
       ,;
   }


private class PersistingStateChangeInterceptor extends   
         StateMachineInterceptorAdapterOrderStatus.OrderStatusChangeEvent {
       // State preprocessing interceptor method
       @Override
       public void preStateChange(State
       
         state, Message
        
          message, Transition
         
           transition, StateMachine
          
            stateMachine)
          ,
         ,
        
       , { listeners.onPersist(state, message, transition, stateMachine); }}private class CompositePersistStateChangeListener extends 
           AbstractCompositeListenerPersistStateChangeListener implements
           PersistStateChangeListener {
       @Override
       public void onPersist(State
       
         state, Message
        
          message, Transition
         
           transition, StateMachine
          
            stateMachine)
          ,
         ,
        
       , {
           for(IteratorPersistStateChangeListener iterator = getListeners().reverse(); iterator.hasNext(); ) { PersistStateChangeListener listener = iterator.next(); listener.onPersist(state, message, transition, stateMachine); }}}}Copy the code

The persistent state change order entity class OrderPersistStateChangeListener implementations of the Listener:

public class OrderPersistStateChangeListener implements 
                  PersistStateMachineHandler.PersistStateChangeListener {

    @Autowired
    private OrderRepo repo;

    @Override
   public void onPersist(State
       
         state, Message
        
          message, Transition
         
           transition, StateMachine
          
            stateMachine)
          ,
         ,
        
       , {
        if(message ! =null  message.getHeaders().containsKey("order")) {
            Integer order = message.getHeaders().get("order", Integer.class); Order o = repo.findByOrderId(order); OrderStatus status = state.getId(); o.setStatus(status); repo.save(o); }}}Copy the code

Springboot injection Handler and Listener bean Configuration class, OrderPersistHandlerConfig

@Configuration
public class OrderPersistHandlerConfig {

    @Autowired
    private StateMachineOrderStatus, OrderStatusChangeEvent stateMachine;


    @Bean
    public OrderStateService persist(a) {
        PersistStateMachineHandler handler = persistStateMachineHandler();
        handler.addPersistStateChangeListener(persistStateChangeListener());
        return new OrderStateService(handler);
    }

    @Bean
    public PersistStateMachineHandler persistStateMachineHandler(a) {
        return new PersistStateMachineHandler(stateMachine);
    }

    @Bean
    public OrderPersistStateChangeListener persistStateChangeListener(a){
        return newOrderPersistStateChangeListener(); }}Copy the code

An example of ControllerService for an order service

The sample provides two simple interfaces, one to view a list of all orders and one to change the status of an order.

The Controller OrderController as follows:

@RestController
@RequestMapping("/orders")
public class OrderController {

    @Autowired
    private OrderStateService orderStateService;

    /** * List all orders **@return* /
    @RequestMapping(method = {RequestMethod.GET})
    public ResponseEntity orders(a) {
        String orders = orderStateService.listDbEntries();
        return new ResponseEntity(orders, HttpStatus.OK);

    }


    /** * Change the status of an order * by triggering an event@param orderId
     * @param event
     * @return* /
    @RequestMapping(value = "/{orderId}", method = {RequestMethod.POST})
    public ResponseEntity processOrderState(@PathVariable("orderId") Integer orderId, @RequestParam("event") OrderStatusChangeEvent event) {
        Boolean result = orderStateService.change(orderId, event);
        return newResponseEntity(result, HttpStatus.OK); }}Copy the code

Order Service Class OrderStateService:

@Component
public class OrderStateService {

    private PersistStateMachineHandler handler;


    public OrderStateService(PersistStateMachineHandler handler) {
        this.handler = handler;
    }

    @Autowired
    private OrderRepo repo;


    public String listDbEntries(a) {
        ListOrder orders = repo.findAll();
        StringJoiner sj = new StringJoiner(",");
        for (Order order : orders) {
            sj.add(order.toString());
        }
        return sj.toString();
    }


    public boolean change(int order, OrderStatusChangeEvent event) {
        Order o = repo.findByOrderId(order);
        return handler.handleEventWithState(MessageBuilder.withPayload(event).setHeader("order", order).build(), o.getStatus()); }}Copy the code
Search
About
mo4tech.com (Moment For Technology) is a global community with thousands techies from across the global hang out!Passionate technologists, be it gadget freaks, tech enthusiasts, coders, technopreneurs, or CIOs, you would find them all here.