什么是状态机

在某个起始状态下,当满足某个条件时,将状态转换到另一个状态的机制。状态机就是实现这一机制的控制元件。状态机的概念也经常出现在电气元件中。

而在软件开发中,状态机指的是一种在某个状态下,由某个事件触发,并将状态转移到目标状态的一个模块。

为什么要使用状态机

状态机解决的痛点在于,当某个业务流程很长,且很复杂时,对于状态的流转的维护成本将膨胀到不可接受的程度。而将状态的流转从业务代码中剥离出来,让开发过程中更加专注于业务处理,就成为了项目急需解决的问题。

状态机的引入,让程序开发不再需要关注复杂繁琐的状态迁移,只需要关注在当前状态及事件下,需要处理的业务逻辑。而状态的迁移交给状态机去完成即可。
简单的例子:一个订单创建后,处于待支付状态。在未使用状态机的情况下,在开发支付的逻辑时,需要关注订单的当前状态是否是待支付,以确保幂等;同时需要关注支付的过程;然后需要关注支付完成后,订单的目标状态。目标状态会有很多种,比如实体货物订单,支付完成后可能要进入待发货状态;而虚拟货物支付完成后,可能就直接进入了待使用状态。在不使用状态机的情况下,支付模块还需要关注订单的下一个状态。

在使用状态机的情况下,开发支付逻辑时,只需要关注支付的业务逻辑,完成支付后,告诉状态机,支付完成即可。状态机会做状态的前置校验,未满足状态时,不会调用业务代码。同时,状态机配置了各种情况下,订单的目标状态。只要支付模块告诉状态机,支付完成,状态机就会按照配置好的状态迁移,将订单转移到指定的状态去。

如何引入状态机

这里以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 .");
    }
}

通过切面的方式,就可以避免修改以前的业务代码,并且引入状态机。需要修改的就是将原来维护状态的代码去掉即可。

以上就是对于状态机使用实践的一个示例。也是我在项目中使用状态机的一点心得。