You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring State Machine到达结束状态后再次获取回退至初始状态求助

Spring State Machine结束状态回退到初始状态问题排查与解决建议

问题场景

在使用Spring State Machine时遇到一个问题:当状态机到达结束状态(如FINISHED)后,第二次通过stateMachineService.acquireStateMachine获取状态机时,状态并未保留在结束状态,反而回退到初始状态CREATED,导致后续状态校验失败。

校验方法代码如下:

public void validateSessionState(Session session, SessionState state) {
    StateMachine<SessionState, SessionEvent> stateMachine =
        stateMachineService.acquireStateMachine(session.getId().toString());
    if (!stateMachine.getState().getId().equals(state)) {
      throw new IllegalStateException(
          "Session with id: "
              + session.getId()
              + " is not "
              + state
              + " but "
              + stateMachine.getState().getId());
    }
}

首次调用该方法时校验正常,但后续调用时校验失败,且状态机的CREATED状态被持久化。

状态机配置类代码:

@Configuration
@EnableStateMachineFactory
@RequiredArgsConstructor
public class SessionStateMachineConfig
    extends EnumStateMachineConfigurerAdapter<SessionState, SessionEvent> {

  private final StateMachineRuntimePersister<SessionState, SessionEvent, String> persister;

  @Override
  public void configure(StateMachineStateConfigurer<SessionState, SessionEvent> states)
      throws Exception {
    states
        .withStates()
        .initial(SessionState.CREATED)
        .end(SessionState.FINISHED)
        .end(SessionState.EXPIRED)
        .states(EnumSet.allOf(SessionState.class));
  }

  @Override
  public void configure(
      final StateMachineConfigurationConfigurer<SessionState, SessionEvent> config)
      throws Exception {
    config.withConfiguration().autoStartup(false);
    config.withPersistence().runtimePersister(persister);
  }

  @Override
  public void configure(StateMachineTransitionConfigurer<SessionState, SessionEvent> transitions)
      throws Exception {
    transitions
        .withExternal()
        .source(SessionState.CREATED)
        .target(SessionState.ACTIVATED)
        .event(SessionEvent.ACTIVATE)
        .action(loggingAction())
        .and()
        .withExternal()
        .source(SessionState.CREATED)
        .target(SessionState.EXPIRED)
        .event(SessionEvent.EXPIRE)
        .action(loggingAction())
        .and()
        .withExternal()
        .source(SessionState.ACTIVATED)
        .target(SessionState.ANSWERING_COMMONS)
        .event(SessionEvent.GET_COMMONS)
        .action(loggingAction())
        .and()
        .withExternal()
        .source(SessionState.ACTIVATED)
        .target(SessionState.EXPIRED)
        .event(SessionEvent.EXPIRE)
        .action(loggingAction())
        .and()
        .withExternal()
        .source(SessionState.ANSWERING_COMMONS)
        .target(SessionState.ANSWERING_SKILL_SET)
        .event(SessionEvent.GET_SKILL_SET)
        .action(loggingAction())
        .and()
        .withExternal()
        .source(SessionState.ANSWERING_COMMONS)
        .target(SessionState.EXPIRED)
        .event(SessionEvent.EXPIRE)
        .action(loggingAction())
        .and()
        .withExternal()
        .source(SessionState.ANSWERING_SKILL_SET)
        .target(SessionState.PROCESSING_QUIZ)
        .event(SessionEvent.GENERATE)
        .action(loggingAction())
        .and()
        .withExternal()
        .source(SessionState.ANSWERING_SKILL_SET)
        .target(SessionState.EXPIRED)
        .event(SessionEvent.EXPIRE)
        .action(loggingAction())
        .and()
        .withExternal()
        .source(SessionState.PROCESSING_QUIZ)
        .target(SessionState.IN_PROGRESS)
        .event(SessionEvent.START)
        .action(loggingAction())
        .and()
        .withExternal()
        .source(SessionState.PROCESSING_QUIZ)
        .target(SessionState.EXPIRED)
        .event(SessionEvent.EXPIRE)
        .action(loggingAction())
        .and()
        .withExternal()
        .source(SessionState.IN_PROGRESS)
        .target(SessionState.PROCESSING_RESULTS)
        .event(SessionEvent.SUBMIT)
        .action(loggingAction())
        .and()
        .withExternal()
        .source(SessionState.IN_PROGRESS)
        .target(SessionState.EXPIRED)
        .event(SessionEvent.EXPIRE)
        .action(loggingAction())
        .and()
        .withExternal()
        .source(SessionState.PROCESSING_RESULTS)
        .target(SessionState.FINISHED)
        .event(SessionEvent.FINISH)
        .action(loggingAction());
  }

  @Bean
  public StateMachineService<SessionState, SessionEvent> stateMachineService(
      final StateMachineFactory<SessionState, SessionEvent> sessionStateMachineFactory,
      final StateMachineRuntimePersister<SessionState, SessionEvent, String>
          sessionsStateMachinePersist) {
    return new DefaultStateMachineService<>(
        sessionStateMachineFactory, sessionsStateMachinePersist);
  }

  @Bean
  public Action<SessionState, SessionEvent> loggingAction() {
    return new LoggingAction();
  }
}

原因分析

  • Spring State Machine默认对结束状态的处理逻辑:当状态机进入结束状态后,会标记自身为completed状态。DefaultStateMachineService在获取状态机实例时,若检测到原实例已完成,会重新创建新的状态机实例,而非从持久化存储中恢复结束状态。
  • 持久化逻辑缺失:如果StateMachineRuntimePersister的实现仅存储了状态ID,未保存状态机的completed标记或结束状态的元数据,恢复时无法识别当前处于结束状态,会默认初始化到初始状态。

解决方案建议

1. 完善持久化逻辑,保留结束状态元数据

确保StateMachineRuntimePersister的实现不仅存储当前状态ID,还需保存状态机的completed状态。在恢复状态机时,先判断是否处于completed状态,若已完成则直接恢复到对应的结束状态,而非初始化到初始状态。

2. 配置状态机的结束状态恢复策略

修改状态机配置,启用停止状态的重启支持,让状态机在恢复时保留结束状态:

@Override
public void configure(StateMachineConfigurationConfigurer<SessionState, SessionEvent> config) throws Exception {
    config.withConfiguration()
        .autoStartup(false)
        .enableStateMachineStoppedRestart(true); // 启用停止状态的重启支持
    config.withPersistence().runtimePersister(persister);
}

3. 手动校验并恢复结束状态

修改校验方法,在获取状态机后先从持久化存储中读取原始状态,若为结束状态则手动恢复:

public void validateSessionState(Session session, SessionState state) {
    String machineId = session.getId().toString();
    // 从持久化存储读取原始上下文
    StateMachineContext<SessionState, SessionEvent> context = persister.read(machineId);
    StateMachine<SessionState, SessionEvent> stateMachine = stateMachineService.acquireStateMachine(machineId);
    
    if (context != null && context.getState().getId().isEndState()) {
        // 恢复结束状态
        stateMachine.stop();
        stateMachine.getStateMachineAccessor().doWithAllRegions(accessor -> {
            accessor.resetStateMachine(context);
        });
        stateMachine.start();
    }
    
    if (!stateMachine.getState().getId().equals(state)) {
        throw new IllegalStateException(
            "Session with id: " + session.getId()
            + " is not " + state + " but " + stateMachine.getState().getId());
    }
}

4. 业务层面优化

当状态机进入结束状态后,直接通过持久化存储的状态进行校验,不再重复获取状态机实例,避免不必要的状态恢复操作。

内容的提问来源于stack exchange,提问作者Artyom Sushchenko

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 17:05:54