什么是状态机?
定义
状态机是有限状态机的简称,是现实事物运行规则抽象而成的一个数学模型。
解释
现实世界中,各种事物都是有状态的,例如人,健康状态、生病状态、痊愈中状态。再比如一个电梯,有停止状态,运行状态。这些状态的转变,都是由于机体的事件触发。人由健康状态到生病状态,会有很多事件,吃错东西了,吃错药,不规则的作息等等等;由生病状态到痊愈中状态,需要看医生事件,吃药事件等。电梯的停止状态到运行状态,需要乘客按下楼层按钮事件等。
本文主要是订单的流转状态来做演示。我们都知道一个电商项目,必然存在的主体就是订单,而一个订单又会有很多状态:待支付(创建)、待发货、待收货、完成、取消等等。
针对以上的状态变换,涉及事件:支付、发货、确认收货、取消。
怎么使用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());
}
}
此处为了做演示,此处逻辑只做简单的状态变更。
发送支付请求:
![80\_1.png][80_1.png]
结果:
![80\_2.png][80_2.png]