什么是状态机
在某个起始状态下,当满足某个条件时,将状态转换到另一个状态的机制。状态机就是实现这一机制的控制元件。状态机的概念也经常出现在电气元件中。
而在软件开发中,状态机指的是一种在某个状态下,由某个事件触发,并将状态转移到目标状态的一个模块。
为什么要使用状态机
状态机解决的痛点在于,当某个业务流程很长,且很复杂时,对于状态的流转的维护成本将膨胀到不可接受的程度。而将状态的流转从业务代码中剥离出来,让开发过程中更加专注于业务处理,就成为了项目急需解决的问题。
状态机的引入,让程序开发不再需要关注复杂繁琐的状态迁移,只需要关注在当前状态及事件下,需要处理的业务逻辑。而状态的迁移交给状态机去完成即可。
简单的例子:一个订单创建后,处于待支付状态。在未使用状态机的情况下,在开发支付的逻辑时,需要关注订单的当前状态是否是待支付,以确保幂等;同时需要关注支付的过程;然后需要关注支付完成后,订单的目标状态。目标状态会有很多种,比如实体货物订单,支付完成后可能要进入待发货状态;而虚拟货物支付完成后,可能就直接进入了待使用状态。在不使用状态机的情况下,支付模块还需要关注订单的下一个状态。
在使用状态机的情况下,开发支付逻辑时,只需要关注支付的业务逻辑,完成支付后,告诉状态机,支付完成即可。状态机会做状态的前置校验,未满足状态时,不会调用业务代码。同时,状态机配置了各种情况下,订单的目标状态。只要支付模块告诉状态机,支付完成,状态机就会按照配置好的状态迁移,将订单转移到指定的状态去。
如何引入状态机
这里以Spring状态机为例,演示如何在项目中使用状态机。
引入状态机包含两种情况,一种是在项目设计的时候,就明确系统需要使用状态机。然而,在敏捷开发的项目里,一般在项目刚起步时,为了能快速迭代,不引入状态机。当业务达到一定的复杂程度,状态维护成本变大时,考虑引入状态机。
下面介绍状态机的几个落地方案。
在pom文件中引入状态机
<!--状态机-->
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-starter</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
下面以一个简单的订单流程为例:
定义订单的状态结点和事件
public enum OrderEvents {
PAY, // 支付
RECEIVE // 收货
}
public enum OrderStates {
UNPAID, // 待支付
WAITING_FOR_RECEIVE, // 待收货
DONE; // 结束
}
构建状态机
@Component
public class OrderStateMachineBuilder {
final static String MACHINEID = "orderMachine";
private final static String INSTALL_MACHINE_ID = "InstallOrderMachine";
/**
* 构建状态机
*
* @param beanFactory
* @return
* @throws Exception
*/
public StateMachine<OrderStates, OrderEvents> build(BeanFactory beanFactory, OrderStates orderState) throws Exception {
StateMachineBuilder.Builder<OrderStates, OrderEvents> builder = StateMachineBuilder.builder();
System.out.println("构建订单状态机");
builder.configureConfiguration()
.withConfiguration()
.machineId(MACHINEID)
.beanFactory(beanFactory);
builder.configureStates()
.withStates()
.initial(orderState)
.states(EnumSet.allOf(OrderStates.class));
builder.configureTransitions()
.withExternal()
.source(OrderStates.UNPAID).target(OrderStates.WAITING_FOR_RECEIVE)
.event(OrderEvents.PAY)
.and()
.withExternal()
.source(OrderStates.WAITING_FOR_RECEIVE).target(OrderStates.DONE)
.event(OrderEvents.RECEIVE);
return builder.build();
}
}
在业务代码中使用状态机
@RestController
@RequestMapping("/statemachine")
public class StateMachineController {
@Resource
private OrderStateMachineBuilder orderStateMachineBuilder;
@Resource
private BeanFactory beanFactory;
@GetMapping("/testOrderState")
public void testOrderState(String orderId) throws Exception {
StateMachine<OrderStates, OrderEvents> stateMachine = orderStateMachineBuilder.build(beanFactory, OrderStates.UNPAID);
System.out.println(stateMachine.getId());
//用message传递数据
Order order = new Order(orderId, "547568678", "广东省深圳市", "13435465465", "RECEIVE");
// 创建流程
stateMachine.start();
// 触发PAY事件
Message<OrderEvents> message = MessageBuilder.withPayload(OrderEvents.PAY).setHeader("order", order).setHeader("otherObj", "otherObjValue").build();
stateMachine.sendEvent(message);
// 触发RECEIVE事件
Message<OrderEvents> message2 = MessageBuilder.withPayload(OrderEvents.RECEIVE).setHeader("order", order).setHeader("otherObj", "otherObjValue").build();
// stateMachine.sendEvent(OrderEvents.RECEIVE);
stateMachine.sendEvent(message2);
// 获取最终状态
System.out.println("最终状态:" + stateMachine.getState().getId());
}
}
注册事件的业务代码
@WithStateMachine(id="orderMachine")
public class OrderEventConfig {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 当前状态UNPAID
*/
@OnTransition(target = "UNPAID")
public void create() {
logger.info("---订单创建,待支付---");
}
/**
* UNPAID->WAITING_FOR_RECEIVE 执行的动作
*/
@OnTransition(source = "UNPAID", target = "WAITING_FOR_RECEIVE")
public void pay(Message<OrderEvents> message) {
System.out.println("传递的参数:" + message.getHeaders().get("order"));
logger.info("---用户完成支付,待收货---");
}
/**
* WAITING_FOR_RECEIVE->DONE 执行的动作
*/
@OnTransition(source = "WAITING_FOR_RECEIVE", target = "DONE")
public void receive(Message<OrderEvents> message) {
System.out.println("传递的参数:" + message.getHeaders().get("order"));
System.out.println("传递的参数:" + message.getHeaders().get("otherObj"));
logger.info("---用户已收货,订单完成---");
}
}
这里展示了状态机的基本用法。但是在实际应用中,状态机的状态不是一口气走完的,这就意味着状态需要持久化。关于状态机的持久化,有两个方向的方案,一个是持久化状态,这个状态在业务系统中,本身也用得到。在每次需要调度状态的时候,按照持久化的状态,构建新的状态机。另外一个是持久化状态机,直接把状态机持久化,下次用的时候,直接取出状态机调度,而不必重新构建状态机。
持久化状态和持久化状态机没有必然的哪个比较好,看业务的使用场景来决定选择哪种持久化方式。
- 通过实现StateMachinePersist接口,持久化状态机
这里使用的是持久化到mysql数据库中,同理,也可以持久化到redis,mangoDB等。
/**
* 在mysql中持久化状态机
*/
@Component
public class InMysqlStateMachinePersist implements StateMachinePersist<OrderStates, OrderEvents, Order> {
@Resource
private OrderStatemachineDao orderStatemachineDao;
@Override
public void write(StateMachineContext<OrderStates, OrderEvents> context, Order order) {
byte[] contextStream = StateMachineSerializeDeserializeUtils.serialize(context);
OrderStatemachine record = new OrderStatemachine();
record.setBusId(Integer.valueOf(order.getId()));
record.setGmtCreate(new Date());
record.setGmtModified(new Date());
record.setCreateBy(order.getUserId());
record.setMachineId(context.getId());
record.setMachineState(contextStream);
OrderStatemachine orderStatemachine = orderStatemachineDao.selectBySelective(Integer.valueOf(order.getId()), record.getMachineId());
if (Objects.isNull(orderStatemachine)) {
orderStatemachineDao.insert(record);
} else {
record.setId(orderStatemachine.getId());
orderStatemachineDao.updateByPrimaryKey(record);
}
}
@Override
public StateMachineContext<OrderStates, OrderEvents> read(Order order) {
StateMachineContext<OrderStates, OrderEvents> context = null;
OrderStatemachine orderStatemachine = orderStatemachineDao.selectBySelective(Integer.valueOf(order.getId()), OrderStateMachineBuilder.MACHINEID);
if (Objects.nonNull(orderStatemachine)) {
context = StateMachineSerializeDeserializeUtils.deserialize(orderStatemachine.getMachineState());
}
return context;
}
}
在业务代码中调用持久化的写入和读取。
@RestController
@RequestMapping("/statemachine")
public class StateMachineController {
@Resource
private OrderStateMachineBuilder orderStateMachineBuilder;
@Resource(name = "orderMysqlPersister")
private StateMachinePersister<OrderStates, OrderEvents, Order> persister;
@Resource
private BeanFactory beanFactory;
@RequestMapping("/testOrderPersister")
public void testOrderPersister(Integer id) throws Exception {
// 创建状态机
StateMachine<OrderStates, OrderEvents> stateMachine = orderStateMachineBuilder.build(beanFactory,OrderStates.UNPAID);
stateMachine.start();
Order order = new Order();
order.setId(String.valueOf(id));
//发送PAY事件
Message<OrderEvents> message = MessageBuilder.withPayload(OrderEvents.PAY).setHeader("order", order).build();
stateMachine.sendEvent(message);
//持久化stateMachine
persister.persist(stateMachine, order);
}
@RequestMapping("/testOrderRestore")
public void testOrderRestore(Integer id) throws Exception {
// 创建状态机
StateMachine<OrderStates, OrderEvents> stateMachine = orderStateMachineBuilder.build(beanFactory,OrderStates.UNPAID);
//订单
Order order = new Order();
order.setId(String.valueOf(id));
persister.restore(stateMachine, order);
//查看恢复后状态机的状态
System.out.println("恢复后的状态:" + stateMachine.getState().getId());
}
}
- 持久化状态
持久化状态,则需要将状态值写入到枚举值当中。如下所示:
public enum OrderStatus {
UNPAID(0, "待支付"), //待支付
WAITING_FOR_RECEIVE(1, "待收货"), // 待收货
DONE(2, "结束"); // 结束
private Integer code;
private String desc;
OrderStatus(int code, String desc) {
this.code = code;
this.desc = desc;
}
public static String getDescByCode(Integer code) {
if (code == null) {
return StaticConstants.EMPTY_STR;
}
for (OrderStatus codeEnum : values()) {
if (codeEnum.code.equals(code)) {
return codeEnum.desc;
}
}
return StaticConstants.EMPTY_STR;
}
public static OrderStatus getStateByCode(Integer code) {
if (code == null) {
return null;
}
for (OrderStatus codeEnum : values()) {
if (codeEnum.code.equals(code)) {
return codeEnum;
}
}
return null;
}
public Integer getCode() {
return code;
}
public String getDesc() {
return desc;
}
}
然后在业务代码中实时生成状态机。
*此处省略了dao层代码。
@RestController
@RequestMapping("/statemachine")
public class StateMachineController {
@Resource
private OrderStateMachineBuilder orderStateMachineBuilder;
@Resource
private BeanFactory beanFactory;
@Resource
private OrderStateMachineDao orderStateMachineDao;
@GetMapping("/testOrderState")
public void testOrderState(String orderId) throws Exception {
Order order = new Order(orderId, "547568678", "广东省深圳市", "13435465465", "RECEIVE");
Integer stateCode = orderStateMachineDao.getStateByOrderId(orderId);
OrderStates states = OrderStatus.getStateByCode(stateCode);
// 创建状态机,并设置当前状态
StateMachine<OrderStates, OrderEvents> stateMachine = orderStateMachineBuilder.build(beanFactory, states);
System.out.println(stateMachine.getId());
//用message传递数据
// 创建流程
stateMachine.start();
// 触发PAY事件
Message<OrderEvents> message = MessageBuilder.withPayload(OrderEvents.PAY).setHeader("order", order).setHeader("otherObj", "otherObjValue").build();
stateMachine.sendEvent(message);
// 触发RECEIVE事件
Message<OrderEvents> message2 = MessageBuilder.withPayload(OrderEvents.RECEIVE).setHeader("order", order).setHeader("otherObj", "otherObjValue").build();
// stateMachine.sendEvent(OrderEvents.RECEIVE);
stateMachine.sendEvent(message2);
// 获取最终状态
System.out.println("最终状态:" + stateMachine.getState().getId());
// 持久化结果状态
Integer retStates = stateMachine.getState().getId().getCode();
orderStateMachineDao.updateStateByOrderId(orderId,retStates);
}
}
状态机的实践应用
状态机已经设计并且实现好了,那么怎么在项目中实践呢?以上实例中,包含了业务代码的填写。然而,对于已经上线的系统,没办法对所有的请求去做改造,对于这种情况,可以使用Spring切面,来实现状态机的嵌入。这里给出一个简单示例。
首先声明一个注解StateMachineAnn
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface StateMachineAnn {
}
然后创建一个aop切面StateMachineAop
@Component
@Aspect
public class StateMachineAop {
@Resource
private OrderStateMachineBuilder orderStateMachineBuilder;
@Resource
private BeanFactory beanFactory;
@Resource
private OrderStateMachineDao orderStateMachineDao;
private static final Logger LOGGER = LoggerFactory.getLogger(StateMachineAop.class);
/**
* 创建切面
* 扫描的包地址,根据自己的包名做相应的变更
*/
@Pointcut("execution(* com.test.domain.order.service.*.*.*(..))")
private void stateMachinePointcut() {
// Do nothing
}
@Around("stateMachinePointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature ms = (MethodSignature) joinPoint.getSignature();
Method targetMethod = ms.getMethod();
LOGGER.info("state machine target method : {}", targetMethod.getName());
// 如果方法上面没有@StateMachineAnn注解,则直接执行方法
if (!targetMethod.isAnnotationPresent(StateMachineAnn.class)) {
// 直接执行方法
return joinPoint.proceed();
}
Object[] args = joinPoint.getArgs();
OrderEvents event = null;
String orderId = null;
if (args == null || args.length < 1) {
String msg = "state machine params error!";
LOGGER.error(msg);
throw new HermesRuntimeException(msg);
}
for(Object arg : args){
if (arg instanceof OrderEvents) {
event = (OrderEvents)arg;
}else if ( arg instanceof Order){
orderId = ((Order) arg).getId();
}
}
LOGGER.info("state machine method params: workOrderId-{},event-{}",orderId,event);
if(event == null || orderId == null){
String msg = "state machine params error!";
LOGGER.error(msg);
throw new HermesRuntimeException(msg);
}
try {
// 查询当前状态
Integer stateCode = orderStateMachineDao.getStateByOrderId(orderId);;
OrderStates state = OrderStates.getStateByCode(stateCode);
if(state == null){
String msg = "state machine current state error!";
LOGGER.error(msg);
throw new HermesRuntimeException(msg);
}
LOGGER.info("state machine current state : {}",state.getDesc());
// 生成状态机
StateMachine<OrderStates, OrderEvents> stateMachine =
orderStateMachineBuilder.build(beanFactory, state);
// 执行事件
// 创建流程
stateMachine.start();
// 触发事件
Message<OrderEvents> message = MessageBuilder.withPayload(event).setHeader("orderId", orderId).build();
stateMachine.sendEvent(message);
// 查询状态结果
OrderStates retState = stateMachine.getState().getId();
stateMachine.stop();
// 如果状态变更成功,则执行目标方法
if(retState != null && !retState.getCode().equals(stateCode)){
LOGGER.info("state machine result state : {}",retState.getDesc());
// 查询结果状态吗
Integer retStateCode = retState.getCode();
// 执行目标方法
Object retObj = joinPoint.proceed();
// 更新状态
orderStateMachineDao.updateStateByOrderId(orderId,retStateCode);
return retObj;
}else {
// 否则报错,并终止方法。
String msg = "state machine result state error!";
LOGGER.error(msg);
throw new HermesRuntimeException(msg);
}
} catch (Throwable throwable) {
// 报错,并终止方法。
String msg = "state machine process error!";
LOGGER.error(msg,throwable);
throw new HermesRuntimeException(msg);
}
}
}
然后在业务service层中,相应的方法上面添加StateMachineAnn注解
@Service
public class InstallMachineAopTest {
@StateMachineAnn
public void testInstallMachine(InstallOrderEvents event, WorkOrderIdVo paramVo){
// Do business statement.
System.out.println("state change .");
}
}
通过切面的方式,就可以避免修改以前的业务代码,并且引入状态机。需要修改的就是将原来维护状态的代码去掉即可。
以上就是对于状态机使用实践的一个示例。也是我在项目中使用状态机的一点心得。