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
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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:
-
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_PAY
Transfer 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.
-
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: