聊一聊 Spring StateMachine 的代码和原理

1,988 阅读9分钟

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

在这篇文章 聊一聊 Spring StateMachine 的基本概念和实践 中,我介绍了 Spring StateMachine 的基本概念和使用,并且通过一个案例对守卫、行动、持久化等特性进行了具体的演示。本篇是姊妹篇,是在实践的基础上进一步探索 Spring StateMachine 内部工作原理的产出。

StateMachine 类的体系结构

状态机的具体实现类是包括 ObjectStateMachine 和 DistributedStateMachine,本篇主要围绕单机版本,DistributedStateMachine 的处理方式主要是引入了 Zookeeper,其他差距不大。在下面的文章中,我将从状态机的启停、事件发生及内部状态流转以及状态机持久化几个角度来分析源码,希望可以结合前一篇的案例,给读者提供更加丰满的理解。下面是 StateMachine 的类结构体系:

StateMachine

在前一篇中也对 region 做了简单的概念介绍:Region 允许在一个状态机中包含多个并行运行的状态子机,意味着同一时间可以在不同的子状态机中处理多个独立的状态转换,提供了一种并行执行状态的机制,它实际上描述的是一种更加复杂的层次关系和状态机之间的并行关系;StateMachine 继承了 Region 接口,意味着每一个状态机都是一个 Region,Region 本身提供了 启、停、事件发送以及状态获取等基本的方法,StateMachine 在 Region 的基础上为通用有限状态机提供了一些新的api,主要是用于处理状态、异常处理以及状态机访问等基本操作。

ObjectStateMachine

ObjectStateMachine 整合了 Spring Bean 的生命周期和 Region 的生命周期,实现了状态机全生命周期托管到 Spring。

Spring StateMachine 的启动和停止

Spring StateMachine 的启动和停止主要涉及到的是 start 和 stop 两个方法,这两个方法并非是继承自 Region 的实现,而是 org.springframework.context.Lifecycle 的 start 和 stop 的实现。

StateMachine#start

当执行 this.stateMachine.start(); 时,实际上调用的是org.springframework.statemachine.support.LifecycleObjectSupport#start方法

@Override
public final void start() {
  this.lifecycleLock.lock();
  try {
    if (!this.running) {
      this.running = true;
      this.doStart();
     	// omitted ..
    }
  } finally {
    this.lifecycleLock.unlock();
  }
}

这里通过继承 org.springframework.context.Lifecycle 接口实现了状态机生命周期和 Sring Bean 生命周期的绑定。start 方法的调用链路如下:

stateMachine.start()

​ -> org.springframework.statemachine.support.LifecycleObjectSupport#start

​ -> org.springframework.statemachine.support.StateMachineObjectSupport#doStart

​ -> org.springframework.statemachine.support.LifecycleObjectSupport#doStart

​ -> org.springframework.statemachine.support.AbstractStateMachine#doStart

这里我们主要分析 StateMachineObjectSupportAbstractStateMachine 两类中的逻辑。

StateMachineObjectSupport#start

@Override
protected void doStart() {
  // org.springframework.statemachine.support.LifecycleObjectSupport#doStart
  super.doStart();
  if (!handlersInitialized) {
    try {
      stateMachineHandlerCallHelper.setBeanFactory(getBeanFactory());
      // 为 org.glmapper.techssm.configs.StateMachineEventConfig 类中的注解生成一组 Handler,具体见下图
      stateMachineHandlerCallHelper.afterPropertiesSet();
    } catch (Exception e) {
      log.error("Unable to initialize annotation handlers", e);
    } finally {
      handlersInitialized = true;
    }
  }
}

这里方法的主要作用就是为上篇中 org.glmapper.techssm.configs.StateMachineEventConfig 类中的如 @OnTransition 等注解生成一组 Handler,如下图所示:

image-20240826155818624

通过 StateMachineHandlerStateMachineRuntimeProcessor 以及 StateMachineMethodInvokerHelper 等相关类及其子类实现当触发某类事件时的反射调用。

AbstractStateMachine#start

@Override
protected void doStart() {
  // 这里调用的是 org.springframework.statemachine.support.StateMachineObjectSupport#doStart
  super.doStart();
  // omitted ... 省略一些代码
  
  // 注册伪状态监听器
  registerPseudoStateListener();
	
  if (initialEnabled != null && !initialEnabled) {
    if (log.isDebugEnabled()) {
      log.debug("Initial disable asked, disabling initial");
    }
    stateMachineExecutor.setInitialEnabled(false);
  } else {
    stateMachineExecutor.setForwardedInitialEvent(forwardedInitialEvent);
  }

  // start fires first execution which should execute initial transition
  stateMachineExecutor.start();
}

上个代码片段中有个有意思的概念:PseudoState;官方注释的描述是:伪状态(PseudoState)是一种抽象概念,包含状态机中不同类型的瞬态或顶点。伪状态通常用于将多个过渡连接成更复杂的状态转换路径。例如,将进入 fork 伪状态的转换与离开 fork 伪状态的一组转换结合起来,我们就得到了一个复合转换,它通向一组正交的目标状态。。这里我通过 idea 看了下 PseudoState 的类结构,发现还有不少这种伪状态。

PseudoState

  • Choice 是一种基于条件的分支状态。它根据某个条件或判断进行状态分支,选择一个合适的目标状态。这类似于编程中的条件判断语句。
  • Fork 允许将状态机的执行分成多个并行路径。进入 Fork 状态后,状态机会同时进入多个目标状态,形成并行的状态执行路径。
  • JoinFork 相对应,它用于将多个并行执行路径合并到一个路径。只有所有并行路径都到达 Join 状态时,状态机才会继续前进。

对于事件初始状态,比如前一篇文章中,我提供了订单的 4 种状态,下面是在执行 registerPseudoStateListener 方法时可以明确的看到运行期的状态信息:

image-20240826162807327

初始状态是 UNPAID,因此它的 PseudoState 会默认增加一个 INITIAL

image-20240826163103903

对于选择类型的,则会增加一个 Choice 类型的 PseudoState (具体可以参考上一篇文章中的代码)。

image-20240826162953478

除注册伪状态监听器之外,比较核心就是启动 StateMachineExecutor,具体逻辑在 org.springframework.statemachine.support.DefaultStateMachineExecutor#doStart

@Override
protected void doStart() {
  // 这里是 org.springframework.statemachine.support.LifecycleObjectSupport#doStart 空方法
  super.doStart();
  // 开启触发器
  startTriggers();
  // 执行
  execute();
}

首先是 startTriggers 方法,Spring StateMachine有两种类型的 Trigger:

  • EventTrigger:基于事件的触发器。当特定的事件被发送到状态机时,EventTrigger 会检查该事件是否与其关联。如果匹配,状态机会根据定义的转换进行状态变更。
  • TimerTrigger:基于时间的触发器。它会在指定的时间条件满足后自动触发状态机的状态转换。

这两个从触发方式上来说是一个外,一个内。最后是 execute 方法,这里实际上就是启动了一个单线程消费一个队列的逻辑。

try {
  	// 用于标记是否处理了事件
    boolean eventProcessed = false;
    // 不断处理事件队列,直到队列为空。对于每个处理的事件,它将eventProcessed设置为true,并处理触发器队列和延迟事件列表
    while (processEventQueue()) {
      eventProcessed = true;
      processTriggerQueue();
      while (processDeferList()) {
        processTriggerQueue();
      }
    }
  	// 如果没有事件被处理,任务处理触发器队列和延迟事件列表。 
    if (!eventProcessed) {
      processTriggerQueue();
      while (processDeferList()) {
        processTriggerQueue();
      }
    }
		// 处理完队列后,任务检查是否有新任务被请求,如果有新任务被请求,它将再次安排处理事件队列。
    if (requestTask.getAndSet(false)) {
      scheduleEventQueueProcessing();
    }
    // 任务通过设置任务引用为null来表示当前任务已经完成,并进行第二次尝试安排处理事件队列,以减少线程导致运行失败的风险。
    taskRef.set(null);
  } 

其中 processEventQueue 涉及到状态转换的逻辑,从上段代码以及下面的代码来看,都用到了 线程 + 阻塞队列的方式实现的一种类似于生产者消费者逻辑的代码。

private boolean processEventQueue() {
		// 省略代码
  
    // 从事件队列中获取并移除队列的头部元素。
		Message<E> queuedEvent = eventQueue.poll();
    // 获取当前状态
		State<S,E> currentState = stateMachine.getState();
		if (queuedEvent != null) {
      // 判断是不是可以延迟
			if ((currentState != null && currentState.shouldDefer(queuedEvent))) {
				log.info("Current state " + currentState + " deferred event " + queuedEvent);
				queueDeferredEvent(queuedEvent);
				return true;
			}
      // 执行 transition,包括源、目标以及事件
			for (Transition<S,E> transition : transitions) {
				State<S,E> source = transition.getSource();
				Trigger<S, E> trigger = transition.getTrigger();

				if (StateMachineUtils.containsAtleastOne(source.getIds(), currentState.getIds())) {
					if (trigger != null && trigger.evaluate(new DefaultTriggerContext<S, E>(queuedEvent.getPayload()))) {
            // 这里是将 trigger 和 queuedEvent 丢到 triggerQueue 中去等待消费
            // triggerQueue 的消费逻辑在 processTriggerQueue 方法中
						queueTrigger(trigger, queuedEvent);
						return true;
					}
				}
			}
			return true;
		}
		return false;
	}

上述的一些核心逻辑均在 DefaultStateMachineExecutorAbstractStateMachine 两个类中;从启动的角度来看,状态机的启动就是将状态机本身和 Spring 绑定,然后启动状态机内部的任务执行器以及一些阻塞队列的初始化,并形成在运行期时能够以生产者消费者模型不断处理不同事件的过程。

Spring StateMachine 的事件发送

事件发送的核心代码也是在 AbstractStateMachine 中,这里先看入口方法

private boolean sendEventInternal(Message<E> event) {
  	// 如果状态机异常,则通知不接受事件
		if (hasStateMachineError()) {
			// TODO: should we throw exception?
			notifyEventNotAccepted(buildStateContext(Stage.EVENT_NOT_ACCEPTED, event, null, getRelayStateMachine(), getState(), null));
			return false;
		}

		try {
      // 状态机拦截器进行拦截处理
			event = getStateMachineInterceptors().preEvent(event, this);
		} catch (Exception e) {
			log.info("Event " + event + " threw exception in interceptors, not accepting event");
			notifyEventNotAccepted(buildStateContext(Stage.EVENT_NOT_ACCEPTED, event, null, getRelayStateMachine(), getState(), null));
			return false;
		}
		// 如果状态机运行完或者不再运行中,则通知不接受事件
		if (isComplete() || !isRunning()) {
			notifyEventNotAccepted(buildStateContext(Stage.EVENT_NOT_ACCEPTED, event, null, getRelayStateMachine(), getState(), null));
			return false;
		}
		boolean accepted = acceptEvent(event);
		stateMachineExecutor.execute();
		if (!accepted) {
      // 如果状态机没有正常接收和处理事件,则通知不接受事件
			notifyEventNotAccepted(buildStateContext(Stage.EVENT_NOT_ACCEPTED, event, null, getRelayStateMachine(), getState(), null));
		}
		return accepted;
	}

这里主要看 acceptEvent 这个方法,其他的均为在一些非常规情况下发送的事件,事件类型均为 EVENT_NOT_ACCEPTED。在 acceptEvent 中,最核心的逻辑在于,会遍历状态机中的所有 transitions,每个 transition 都有一个源状态和一个触发器,并对当前的状态做一些必要的检查;这里也会涉及到 Guard 的校验,如果都通过,则当前转换事件会被丢到 stateMachineExecutoreventQueue 中。

所以事件发送就是从外部发起一个触发条件,在提交到 stateMachineExecutor 和相关的队列之前做一些比较的校验。当事件被推送到 stateMachineExecutor 的相关队列之后,相关的逻辑就是上一个小节中启动处理的逻辑。

Spring StateMachine 的持久化

关于持久化部分,可以分两块来看,一种是 StateMachinePersister,还有一种是 StateMachinePersist ,是的看起来很像,但是从提供的接口方法来看,还是完全不同的。

  • StateMachinePersist 体系

StateMachinePersist

  • StateMachinePersister 体系

StateMachinePersister

StateMachinePersister 提供的是 persist 和 restore 一对方法,主要是将 StateMachine 进行持久化和恢复。StateMachinePersist 提供的是 write 和 read 一对方法,他关注的是 StateMachineContext,而不是 StateMachine。实际上从源码角度,StateMachinePersister 的 persist 和 restore 方法会委托给 StateMachinePersist 的 write 和 read 方法。如 AbstractStateMachinePersister 中的逻辑

@Override
public final void persist(StateMachine<S, E> stateMachine, T contextObj) throws Exception {
  // 使用 stateMachinePersist.write
  stateMachinePersist.write(buildStateMachineContext(stateMachine), contextObj);
}

@Override
public final StateMachine<S, E> restore(StateMachine<S, E> stateMachine, T contextObj) throws Exception {
  // 使用 stateMachinePersist.read
  final StateMachineContext<S, E> context = stateMachinePersist.read(contextObj);
  // 省略其他代码 ...
}

关于持久化部分,最后在来看下序列化的逻辑; Spring 状态机仅提供了 Kryo 一种序列化方式。具体实现类是 KryoStateMachineSerialisationService。它提供了 doEncodedoDecode 两个基本的方法,底层则是直接依赖 com.esotericsoftware.kryo.Kryo#writeObjectcom.esotericsoftware.kryo.Kryo#writeObject 两个方法来支持具体的对象编解码工作。

总结

本篇文章是 聊一聊 Spring StateMachine 的基本概念和实践 的延续,从实践到源码分析;关于 Spring StateMachine 其核心代码不算复杂,重要的是需要先理解概念再理解状态机模型。本质上内部通过事件发布、订阅和线程池 + 阻塞队列实现了整个状态的流转、执行拦截、事件通知回调以及转换校验等。