What is a state machine?

define

State machine is short for finite state machine. It is a mathematical model abstracted from the operation rules of real things.

explain

In the real world, all kinds of things have states, such as people, healthy state, sick state, and healing state. Another example is an elevator, which has a stop state, a running state. These state changes are triggered by events in the body. From the state of health to the state of illness, there will be a lot of events, eat the wrong food, take the wrong medicine, irregular work and rest and so on; From the state of illness to the state of recovery, need to see a doctor events, events such as medication. From the stop state to the running state of the elevator, it is necessary for passengers to press the floor button.

This article is mainly the order flow state to do the demonstration. As we all know, the subject of an e-commerce project is the order, and an order will have many states: to be paid (created), to be delivered, to be received, completed, cancelled and so on.

For the above state changes, the events involved are: payment, shipment, confirmation of receipt, and cancellation.

How to use Spring StateMachine?

Basic configuration

  1. First, poM files introduce dependencies

          <dependency>
                <groupId>org.springframework.statemachine</groupId>
                <artifactId>spring-statemachine-core</artifactId>
                <version>2.2.0. RELEASE</version>
           </dependency>
    Copy the code
  2. Write the order status class

    package com.yezi.statemachinedemo.business.enums;
    
    / * * *@Description: Order status *@Author: yezi
     * @Date: 2020/6/19 14:01 * /
    public enum TradeStatus {
        / / to pay
        TO_PAY,
        / / momentum
        TO_DELIVER,
        / / for the goods
        TO_RECIEVE,
        / / finish
        COMPLETE,
        / / cancel
        VOID;
    }
    
    Copy the code
  3. State flow involves events

    package com.yezi.statemachinedemo.business.enums;
    
    / * * *@Description: Order event *@Author: yezi
     * @Date: 2020/6/19 14:02 * /
    public enum TradeEvent {
        PAY, / / pay
        SHIP,/ / delivery
        CONFIRM,// Confirm receipt of goods
        VOID/ / cancel
    }
    
    Copy the code
  4. Write order entity

    package com.yezi.statemachinedemo.business.entity;
    
    import com.yezi.statemachinedemo.business.enums.TradeStatus;
    import lombok.Data;
    
    import javax.persistence.*;
    import java.time.LocalDateTime;
    
    / * * *@Description:
     * @Author: yezi
     * @Date: 2020/6/19 13:56 * /
    @Data
    @Entity
    @Table(name = "trade")
    public class Trade {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        /** * Order status */
        @Enumerated(value = EnumType.STRING)
        private TradeStatus status;
    
        /** * Order number */
        private String tradeNo;
    
        /** * create time */
        private LocalDateTime createTime;
    
    }
    
    Copy the code

The core configuration

  1. Order status mechanism builder

    package com.yezi.statemachinedemo.fsm;
    
    import com.yezi.statemachinedemo.business.entity.Trade;
    import com.yezi.statemachinedemo.business.enums.TradeEvent;
    import com.yezi.statemachinedemo.business.enums.TradeStatus;
    import org.springframework.beans.factory.BeanFactory;
    import org.springframework.statemachine.StateMachine;
    
    / * * *@Description: Order status mechanism builder *@Author: yezi
     * @Date: 2020/6/22 and * /
    public interface TradeFSMBuilder {  
    
        / * * *@return* /
        TradeStatus supportState(a);
    
        / * * *@param trade
         * @param beanFactory
         * @return
         * @throws Exception
         */
        StateMachine<TradeStatus, TradeEvent> build(Trade trade, BeanFactory beanFactory) throws Exception;
    }
    
    Copy the code
  2. Provide a state machine factory to create different state machines, where Spring automatically injects all the implementation classes of the state machine builder into tradeFSMBuilders, implements the InitializingBean interface, and stores the state machine build into builderMap while the factory class is instantiated.

    package com.yezi.statemachinedemo.fsm;
    
    
    import com.yezi.statemachinedemo.business.entity.Trade;
    import com.yezi.statemachinedemo.business.enums.TradeEvent;
    import com.yezi.statemachinedemo.business.enums.TradeStatus;
    import org.springframework.beans.factory.BeanFactory;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.statemachine.StateMachine;
    import org.springframework.stereotype.Component;
    
    import java.util.List;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.function.Function;
    import java.util.stream.Collectors;
    
    / * * *@Description: State machine factory *@Author: yezi
     * @Date: 2020/6/19 17:13
     */
    @Component
    public class BuilderFactory implements InitializingBean {
    
        private Map<TradeStatus, TradeFSMBuilder> builderMap = new ConcurrentHashMap<>();
    
    
        @Autowired
        private List<TradeFSMBuilder> tradeFSMBuilders;
    
    
        @Autowired
        private BeanFactory beanFactory;
    
    
        public StateMachine<TradeStatus, TradeEvent> create(Trade trade) {
            TradeStatus tradeStatus = trade.getStatus();
            TradeFSMBuilder tradeFSMBuilder = builderMap.get(tradeStatus);
            if (tradeFSMBuilder == null) {
                throw new RuntimeException("Builder creation failed");
            }
            // Create an order state machine
            StateMachine<TradeStatus, TradeEvent> sm;
            try {
                sm = tradeFSMBuilder.build(trade, beanFactory);
                sm.start();
            } catch (Exception e) {
                throw new RuntimeException("State machine creation failed");
            }
            // Put the order into the state machine
            sm.getExtendedState().getVariables().put(Trade.class, trade);
            return sm;
        }
    
        @Override
        public void afterPropertiesSet(a) throws Exception { builderMap = tradeFSMBuilders.stream().collect(Collectors.toMap(TradeFSMBuilder::supportState, Function.identity())); }}Copy the code
  3. Write the state machine service class

    package com.yezi.statemachinedemo.fsm;
    
    import com.yezi.statemachinedemo.business.entity.Trade;
    import com.yezi.statemachinedemo.business.enums.TradeEvent;
    import com.yezi.statemachinedemo.business.enums.TradeStatus;
    import com.yezi.statemachinedemo.service.TradeService;
    import com.yezi.statemachinedemo.fsm.params.StateRequest;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.statemachine.StateMachine;
    import org.springframework.stereotype.Service;
    
    import java.util.Objects;
    
    / * * *@Description: Order state machine service *@Author: yezi
     * @Date: 2020/6/22 and * /
    @Slf4j
    @Service
    public class TradeFSMService {
    
        @Autowired
        private TradeService tradeService;
    
        @Autowired
        private BuilderFactory builderFactory;
    
        /** * Order status changed **@param request
         * @return* /
        public boolean changeState(StateRequest request) {
            Trade trade = tradeService.findById(request.getTid());
            log.info("trade={}", trade);
            if (Objects.isNull(trade)) {
                log.error("Failed to create order state machine, unable to transition from state {} => {}", trade.getStatus(), request.getEvent());
                throw new RuntimeException("Order does not exist");
            }
            //1. Create a state machine based on the order
            StateMachine<TradeStatus, TradeEvent> stateMachine = builderFactory.create(trade);
            //2. Pass the parameters to the state machine
            stateMachine.getExtendedState().getVariables().put(StateRequest.class, request);
            //3. Send the current request status
            boolean isSend = stateMachine.sendEvent(request.getEvent());
            if(! isSend) { log.error("Failed to create order state machine, unable to transition from state {} => {}", trade.getStatus(), request.getEvent());
                throw new RuntimeException("Failed to create order state machine");
            }
            //4. Check whether exceptions occur during processing
            Exception exception = stateMachine.getExtendedState().get(Exception.class, Exception.class);
            if(exception ! =null) {
                if (exception.getClass().isAssignableFrom(RuntimeException.class)) {
                    throw (RuntimeException) exception;
                } else {
                    throw new RuntimeException("Abnormal state machine processing"); }}return true; }}Copy the code
  4. State flow action class

    package com.yezi.statemachinedemo.fsm;
    
    import com.yezi.statemachinedemo.business.entity.Trade;
    import com.yezi.statemachinedemo.business.enums.TradeEvent;
    import com.yezi.statemachinedemo.business.enums.TradeStatus;
    import com.yezi.statemachinedemo.service.TradeService;
    import com.yezi.statemachinedemo.fsm.params.StateRequest;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.statemachine.StateContext;
    import org.springframework.statemachine.action.Action;
    
    import java.lang.reflect.UndeclaredThrowableException;
    
    / * * *@Description:
     * @Author: yezi
     * @Date: 2020/6/22 but * /
    @Slf4j
    public abstract class TradeAction implements Action<TradeStatus.TradeEvent> {
    
        @Autowired
        private TradeService tradeService;
    
        @Override
        public void execute(StateContext<TradeStatus, TradeEvent> stateContext) {
            TradeStateContext tsc = new TradeStateContext(stateContext);
            try {
                evaluateInternal(tsc.getTrade(), tsc.getRequest(), tsc);
            } catch (Exception e) {
                // Catch the exception here and put the exception information into the order status machine
                tsc.put(Exception.class, e);
                if (e instanceof UndeclaredThrowableException) {
                    // If a package exception occurs, obtain the specific exception information
                    Throwable undeclaredThrowable = ((UndeclaredThrowableException) e).getUndeclaredThrowable();
                    undeclaredThrowable.printStackTrace();
                    log.error(String.format("The order processing, from state [% s], [% s] after event, to state [% s], abnormal [% s]", stateContext.getSource().getId(), stateContext.getEvent(), stateContext.getTarget().getId(), undeclaredThrowable));
                } else {
                    e.printStackTrace();
                    log.error(String.format("The order processing, from state [% s], [% s] after event, to state [% s], abnormal [% s]", stateContext.getSource().getId(), stateContext.getEvent(), stateContext.getTarget().getId(), e)); }}}/** * update order **@param trade
         */
        protected void update(Trade trade) {
            tradeService.update(trade);
        }
    
    
        protected abstract void evaluateInternal(Trade trade, StateRequest request, TradeStateContext tsc);
    }
    
    Copy the code
  5. Context on the state machine. The context on the current state machine is mainly used to store abnormal information during order processing

    package com.yezi.statemachinedemo.fsm;
    
    
    import com.yezi.statemachinedemo.business.entity.Trade;
    import com.yezi.statemachinedemo.business.enums.TradeEvent;
    import com.yezi.statemachinedemo.business.enums.TradeStatus;
    import com.yezi.statemachinedemo.fsm.params.StateRequest;
    import org.springframework.statemachine.StateContext;
    import org.springframework.statemachine.StateMachine;
    
    / * * *@Description: Order status context: The packaging of the context on the current state machine, mainly used to store the abnormal information in the process of order processing *@Author: yezi
     * @Date: 2020/6/22 but * /
    public class TradeStateContext {
    
        private StateContext<TradeStatus, TradeEvent> stateContext;
    
        public TradeStateContext(StateContext<TradeStatus, TradeEvent> stateContext) {
            this.stateContext = stateContext;
        }
    
        /** * Puts exceptions that occur during order processing into the order status context **@param key
         * @param value
         * @return* /
        public TradeStateContext put(Object key, Object value) {
            stateContext.getExtendedState().getVariables().put(key, value);
            return this;
        }
    
        /** * Gets the order ** processed by the current state machine@return* /
        public Trade getTrade(a) {
            return this.stateContext.getExtendedState().get(Trade.class, Trade.class);
        }
    
        /** * Gets the request ** being processed by the current state machine@return* /
        public StateRequest getRequest(a) {
            return this.stateContext.getExtendedState().get(StateRequest.class, StateRequest.class);
        }
    
        /** * Get operator information **@return* /
        public String getOperator(a) {
            return getRequest().getOperator();
        }
    
        /** * request data **@param <T>
         * @return* /
        public <T> T getRequestData(a) {
            return (T) getRequest().getData();
        }
    
        /** * Current state machine **@return* /
        public StateMachine<TradeStatus, TradeEvent> getStateMachine(a) {
            return this.stateContext.getStateMachine();
        }
    
        /** ** below ** on the current state machine@return* /
        public StateContext<TradeStatus, TradeEvent> getStateContext(a) {
            returnstateContext; }}Copy the code

The above are the core configuration classes used in this article, which involve some design patterns that I will not cover in detail.

The sample

Since there are many order status flows, only one of them is selected for demonstration. Take order payment as an example:

  1. Write order payment status mechanism builder

    package com.yezi.statemachinedemo.fsm.builder;
    
    import com.yezi.statemachinedemo.business.entity.Trade;
    import com.yezi.statemachinedemo.business.enums.TradeEvent;
    import com.yezi.statemachinedemo.business.enums.TradeStatus;
    import com.yezi.statemachinedemo.fsm.TradeFSMBuilder;
    import com.yezi.statemachinedemo.fsm.action.CancelAction;
    import com.yezi.statemachinedemo.fsm.action.PayAction;
    import org.springframework.beans.factory.BeanFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.statemachine.StateMachine;
    import org.springframework.statemachine.config.StateMachineBuilder;
    import org.springframework.stereotype.Component;
    
    import java.util.EnumSet;
    
    /**
     * @Description:
     * @Author: yezi
     * @Date: 2020/6/19 17:13
     */
    @Component
    public class PayTradeFSMBuilder implements TradeFSMBuilder {
    
    
        @Autowired
        private PayAction payAction;
    
        @Autowired
        private CancelAction cancelAction;
    
    
        @Override
        public TradeStatus supportState() {
            returnTradeStatus.TO_PAY; } @Override public StateMachine<TradeStatus, TradeEvent> build(Trade trade, BeanFactory beanFactory) throws Exception { StateMachineBuilder.Builder<TradeStatus, TradeEvent> builder = StateMachineBuilder.builder(); builder.configureStates() .withStates() .initial(TradeStatus.TO_PAY) .states(EnumSet.allOf(TradeStatus.class)); Builder. ConfigureTransitions () / / to pay - > delivery. WithExternal (). The source (TradeStatus. TO_PAY). The target (TradeStatus. TO_DELIVER) .event(tradeevent.pay).action(payAction). And () // to be paid -> cancel.withexternal () .source(TradeStatus.TO_PAY).target(TradeStatus.VOID) .event(TradeEvent.VOID) .action(cancelAction);returnbuilder.build(); }}Copy the code

    The order to be paid currently has two status flows, one is delivery after payment, the other is only cancellation; The two states are parallel but they’re performing different actions.

    • initial(TradeStatus.TO_PAY)Indicates that the initial status is notTO_PAY.
    • source(TradeStatus.TO_PAY).target(TradeStatus.TO_DELIVER)Represents by stateTO_PAYTransfer toTO_DELIVER.
    • event(TradeEvent.PAY)Represents the trigger event.
    • action(payAction)Represents the execution of the action, which is the actual business logic.
    • If a state has multiple state flows, Spring Statemachine supports the use of chained programming, with different events starting different actions.
  2. Write order payment actions

    package com.yezi.statemachinedemo.fsm.action; import com.yezi.statemachinedemo.business.entity.Trade; import com.yezi.statemachinedemo.business.enums.TradeStatus; import com.yezi.statemachinedemo.fsm.TradeAction; import com.yezi.statemachinedemo.fsm.TradeStateContext; import com.yezi.statemachinedemo.fsm.params.StateRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; /** * @description: order payment action * @author: yezi * @date: 2020/6/22 15:22 */ @Slf4j @Component public class PayAction extends TradeAction { @Override protected void evaluateInternal(Trade trade, StateRequest request, TradeStateContext tsc) { pay(trade); } /** * @param trade */ private void pay(trade trade) {trade.setStatus(tradestatus.to_deliver); update(trade); log.info("Order number {}, payment successful.", trade.getTradeNo()); }}Copy the code

    For demonstration purposes, the logic here does only a simple state change.

    Send payment request:

    Results: