Spring StateMachine状态机

2,394 阅读6分钟

什么是状态机?

定义

状态机是有限状态机的简称,是现实事物运行规则抽象而成的一个数学模型。

解释

现实世界中,各种事物都是有状态的,例如人,健康状态、生病状态、痊愈中状态。再比如一个电梯,有停止状态,运行状态。这些状态的转变,都是由于机体的事件触发。人由健康状态到生病状态,会有很多事件,吃错东西了,吃错药,不规则的作息等等等;由生病状态到痊愈中状态,需要看医生事件,吃药事件等。电梯的停止状态到运行状态,需要乘客按下楼层按钮事件等。

本文主要是订单的流转状态来做演示。我们都知道一个电商项目,必然存在的主体就是订单,而一个订单又会有很多状态:待支付(创建)、待发货、待收货、完成、取消等等。

针对以上的状态变换,涉及事件:支付、发货、确认收货、取消。

怎么使用spring StateMachine?

基础配置

  1. 首先pom文件引入依赖

          <dependency>
                <groupId>org.springframework.statemachine</groupId>
                <artifactId>spring-statemachine-core</artifactId>
                <version>2.2.0.RELEASE</version>
           </dependency>
    
  2. 编写订单状态类

    package com.yezi.statemachinedemo.business.enums;
    
    /**
     * @Description: 订单状态
     * @Author: yezi
     * @Date: 2020/6/19 14:01
     */
    public enum TradeStatus {
        //待支付
        TO_PAY,
        //待发货
        TO_DELIVER,
        //待收货
        TO_RECIEVE,
        //完成
        COMPLETE,
        //取消
        VOID;
    }
    
    
  3. 状态流转会涉及的到事件

    package com.yezi.statemachinedemo.business.enums;
    
    /**
     * @Description: 订单事件
     * @Author: yezi
     * @Date: 2020/6/19 14:02
     */
    public enum TradeEvent {
        PAY, //支付
        SHIP,//发货
        CONFIRM,//确认收货
        VOID//取消
    }
    
    
  4. 编写订单实体

    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;
    
        /**
         * 订单状态
         */
        @Enumerated(value = EnumType.STRING)
        private TradeStatus status;
    
        /**
         * 订单号
         */
        private String tradeNo;
    
        /**
         * 创建时间
         */
        private LocalDateTime createTime;
    
    }
    
    

核心配置

  1. 订单状态机构建器

    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: 订单状态机构建器
     * @Author: yezi
     * @Date: 2020/6/22 15:24
     */
    public interface TradeFSMBuilder {  
    
        /**
         * @return
         */
        TradeStatus supportState();
    
        /**
         * @param trade
         * @param beanFactory
         * @return
         * @throws Exception
         */
        StateMachine<TradeStatus, TradeEvent> build(Trade trade, BeanFactory beanFactory) throws Exception;
    }
    
    
  2. 提供一个状态机工厂用以创建不同的状态机,这里spring会将状态机构建器的所有实现类自动注入tradeFSMBuilders,同时实现InitializingBean接口,在工厂类实例化同时将状态机构建起存入builderMap中。

    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: 状态机工厂
     * @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("构建器创建失败");
            }
            //创建订单状态机
            StateMachine<TradeStatus, TradeEvent> sm;
            try {
                sm = tradeFSMBuilder.build(trade, beanFactory);
                sm.start();
            } catch (Exception e) {
                throw new RuntimeException("状态机创建失败");
            }
            //将订单放入状态机
            sm.getExtendedState().getVariables().put(Trade.class, trade);
            return sm;
        }
    
        @Override
        public void afterPropertiesSet() throws Exception {
            builderMap = tradeFSMBuilders.stream().collect(Collectors.toMap(TradeFSMBuilder::supportState, Function.identity()));
        }
    }
    
  3. 编写状态机服务类

    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: 订单状态机服务
     * @Author: yezi
     * @Date: 2020/6/22 15:24
     */
    @Slf4j
    @Service
    public class TradeFSMService {
    
        @Autowired
        private TradeService tradeService;
    
        @Autowired
        private BuilderFactory builderFactory;
    
        /**
         * 订单状态变更
         *
         * @param request
         * @return
         */
        public boolean changeState(StateRequest request) {
            Trade trade = tradeService.findById(request.getTid());
            log.info("trade={}", trade);
            if (Objects.isNull(trade)) {
                log.error("创建订单状态机失败,无法从状态 {} 转向 => {}", trade.getStatus(), request.getEvent());
                throw new RuntimeException("订单不存在");
            }
            //1.根据订单创建状态机
            StateMachine<TradeStatus, TradeEvent> stateMachine = builderFactory.create(trade);
            //2.将参数传入状态机
            stateMachine.getExtendedState().getVariables().put(StateRequest.class, request);
            //3.发送当前请求的状态
            boolean isSend = stateMachine.sendEvent(request.getEvent());
            if (!isSend) {
                log.error("创建订单状态机失败,无法从状态 {} 转向 => {}", trade.getStatus(), request.getEvent());
                throw new RuntimeException("创建订单状态机失败");
            }
            //4. 判断处理过程中是否出现了异常
            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("状态机处理出现异常");
                }
            }
            return true;
        }
    }
    
    
  4. 状态流转动作类

    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 15:13
     */
    @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) {
                //捕获此处异常,将异常信息放入订单状态机上下文
                tsc.put(Exception.class, e);
                if (e instanceof UndeclaredThrowableException) {
                    //如果发生包装异常,需要获取包装异常中的具体异常信息
                    Throwable undeclaredThrowable = ((UndeclaredThrowableException) e).getUndeclaredThrowable();
                    undeclaredThrowable.printStackTrace();
                    log.error(String.format("订单处理, 从状态[ %s ], 经过事件[ %s ], 到状态[ %s ], 出现异常[ %s ]", stateContext.getSource().getId(), stateContext.getEvent(), stateContext.getTarget().getId(), undeclaredThrowable));
                } else {
                    e.printStackTrace();
                    log.error(String.format("订单处理, 从状态[ %s ], 经过事件[ %s ], 到状态[ %s ], 出现异常[ %s ]", stateContext.getSource().getId(), stateContext.getEvent(), stateContext.getTarget().getId(), e));
                }
    
            }
        }
    
    
        /**
         * 更新订单
         *
         * @param trade
         */
        protected void update(Trade trade) {
            tradeService.update(trade);
        }
    
    
        protected abstract void evaluateInternal(Trade trade, StateRequest request, TradeStateContext tsc);
    }
    
    
  5. 状态机上下文,对当前状态机上下文的包装,主要作用于存放订单处理过程中出现的异常信息

    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: 订单状态上下文:对当前状态机上下文的包装,主要作用于存放订单处理过程中出现的异常信息
     * @Author: yezi
     * @Date: 2020/6/22 15:13
     */
    public class TradeStateContext {
    
        private StateContext<TradeStatus, TradeEvent> stateContext;
    
        public TradeStateContext(StateContext<TradeStatus, TradeEvent> stateContext) {
            this.stateContext = stateContext;
        }
    
        /**
         * 将订单处理过程中发生的异常放入订单状态上下文
         *
         * @param key
         * @param value
         * @return
         */
        public TradeStateContext put(Object key, Object value) {
            stateContext.getExtendedState().getVariables().put(key, value);
            return this;
        }
    
        /**
         * 获取当前状态机所处理的订单
         *
         * @return
         */
        public Trade getTrade() {
            return this.stateContext.getExtendedState().get(Trade.class, Trade.class);
        }
    
        /**
         * 获取当前状态机所处理的请求
         *
         * @return
         */
        public StateRequest getRequest() {
            return this.stateContext.getExtendedState().get(StateRequest.class, StateRequest.class);
        }
    
        /**
         * 获取操作人信息
         *
         * @return
         */
        public String getOperator() {
            return getRequest().getOperator();
        }
    
        /**
         * 请求数据
         *
         * @param <T>
         * @return
         */
        public <T> T getRequestData() {
            return (T) getRequest().getData();
        }
    
        /**
         * 当前状态机
         *
         * @return
         */
        public StateMachine<TradeStatus, TradeEvent> getStateMachine() {
            return this.stateContext.getStateMachine();
        }
    
        /**
         * 当前状态机上下文
         *
         * @return
         */
        public StateContext<TradeStatus, TradeEvent> getStateContext() {
            return stateContext;
        }
    }
    
    

以上为本文用到的核心配置类,当中涉及一些设计模式,就不一一介绍了。

示例

由于订单的状态流转众多,为了演示,只选取其中一种做演示。下面以订单支付为例:

  1. 编写订单支付状态机构建器

    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() {
            return TradeStatus.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()
                    //待支付 -> 发货
                    .withExternal()
                    .source(TradeStatus.TO_PAY).target(TradeStatus.TO_DELIVER)
                    .event(TradeEvent.PAY)
                    .action(payAction)
                    .and()
                    //待支付 -> 取消
                    .withExternal()
                    .source(TradeStatus.TO_PAY).target(TradeStatus.VOID)
                    .event(TradeEvent.VOID)
                    .action(cancelAction);
    
            return builder.build();
        }
    }
    
    

    待支付状态的订单当前有2种状态流转,一个是支付之后发货,一个只取消;2个状态是平行状态只是执行的动作不同。

    • initial(TradeStatus.TO_PAY)表示初始状态未TO_PAY
    • source(TradeStatus.TO_PAY).target(TradeStatus.TO_DELIVER)表示由状态TO_PAY流转为TO_DELIVER
    • event(TradeEvent.PAY)表示触发事件。
    • action(payAction)表示执行动作,也就是实际的业务逻辑。
    • 下面还有待支付到取消,如果一个状态会有多种状态流转,spring statemachine支持使用类似链式编程的方式,由不同事件出发不同动作。
  2. 编写订单支付动作

    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: 订单支付动作
     * @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("订单号{},支付成功。", trade.getTradeNo());
        }
    }
    
    

    此处为了做演示,此处逻辑只做简单的状态变更。

    发送支付请求:

    结果: