什么是状态机?
定义
状态机是有限状态机的简称,是现实事物运行规则抽象而成的一个数学模型。
解释
现实世界中,各种事物都是有状态的,例如人,健康状态、生病状态、痊愈中状态。再比如一个电梯,有停止状态,运行状态。这些状态的转变,都是由于机体的事件触发。人由健康状态到生病状态,会有很多事件,吃错东西了,吃错药,不规则的作息等等等;由生病状态到痊愈中状态,需要看医生事件,吃药事件等。电梯的停止状态到运行状态,需要乘客按下楼层按钮事件等。
本文主要是订单的流转状态来做演示。我们都知道一个电商项目,必然存在的主体就是订单,而一个订单又会有很多状态:待支付(创建)、待发货、待收货、完成、取消等等。
针对以上的状态变换,涉及事件:支付、发货、确认收货、取消。
怎么使用spring StateMachine?
基础配置
-
首先pom文件引入依赖
<dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-core</artifactId> <version>2.2.0.RELEASE</version> </dependency> -
编写订单状态类
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; } -
状态流转会涉及的到事件
package com.yezi.statemachinedemo.business.enums; /** * @Description: 订单事件 * @Author: yezi * @Date: 2020/6/19 14:02 */ public enum TradeEvent { PAY, //支付 SHIP,//发货 CONFIRM,//确认收货 VOID//取消 } -
编写订单实体
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; }
核心配置
-
订单状态机构建器
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; } -
提供一个状态机工厂用以创建不同的状态机,这里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())); } } -
编写状态机服务类
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; } } -
状态流转动作类
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); } -
状态机上下文,对当前状态机上下文的包装,主要作用于存放订单处理过程中出现的异常信息
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; } }
以上为本文用到的核心配置类,当中涉及一些设计模式,就不一一介绍了。
示例
由于订单的状态流转众多,为了演示,只选取其中一种做演示。下面以订单支付为例:
-
编写订单支付状态机构建器
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支持使用类似链式编程的方式,由不同事件出发不同动作。
-
编写订单支付动作
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()); } }此处为了做演示,此处逻辑只做简单的状态变更。
发送支付请求:
结果: