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

Spring StateMachineFactory获取状态机始终返回初始状态:如何为每个员工维护独立状态机实例

解决Spring State Machine为每个员工维护独立实例的问题

看起来你碰到的核心问题是:无法让同一个员工对应的状态机实例保持状态一致性——要么每次获取到新实例(状态重置为初始),要么实例是同一个但状态没保留。下面我会一步步拆解问题根源,给出具体的解决方案:


问题根源分析

先看你测试用例的结果:

  1. 用UUID作为ID时,两次获取的是同一个实例(UUID相同),但状态不同——这是因为状态机的状态没有被持久化,且你没有在获取时恢复状态;
  2. 用字符串ID(如"1")时,每次都是新实例(UUID不同)——因为默认的StateMachineFactory不会缓存实例,每次调用getStateMachine()都会创建全新的状态机。

你的需求是每个员工对应唯一的状态机实例,且实例状态与员工持久化状态同步,所以需要解决两个核心问题:实例缓存、状态持久化与恢复。


解决方案步骤

1. 配置状态机实例缓存

首先要让工厂根据员工ID缓存状态机实例,避免重复创建。我们可以用Spring State Machine提供的StateMachineCache实现:

修改你的StateMachineConfig类,添加缓存配置:

@Configuration
@EnableStateMachineFactory
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<EmployeeState, EmployeeEvent> {

    // 注入EmployeeRepository,后面持久化会用到
    private final EmployeeRepository employeeRepository;

    public StateMachineConfig(EmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }

    @Override
    public void configure(StateMachineConfigurationConfigurer<EmployeeState, EmployeeEvent> config) throws Exception {
        config
            .withConfiguration()
            .autoStartup(true)
            .listener(listener())
            .stateMachineCache(stateMachineCache()) // 新增:配置实例缓存
            .and()
            .withPersistence()
            .persist(stateMachinePersist()) // 新增:配置状态持久化
            .restore(true); // 新增:启动时自动恢复状态
    }

    // 实现状态机缓存:用ConcurrentHashMap存储实例,key为员工ID字符串
    @Bean
    public StateMachineCache<EmployeeState, EmployeeEvent> stateMachineCache() {
        return new DefaultStateMachineCache<>(new ConcurrentHashMap<>());
    }

    // 其他原有配置(states、transitions、listener)保持不变
}

2. 实现状态持久化与恢复

接下来要让状态机的状态和员工数据库中的状态同步,需要自定义StateMachinePersist实现:

在StateMachineConfig中添加以下Bean:

// 实现状态机状态的持久化与读取
@Bean
public StateMachinePersist<EmployeeState, EmployeeEvent, String> stateMachinePersist() {
    return new StateMachinePersist<>() {
        @Override
        public void write(StateMachineContext<EmployeeState, EmployeeEvent> context, String employeeIdStr) throws Exception {
            // 状态变化时,将状态同步到员工数据库
            Long employeeId = Long.parseLong(employeeIdStr);
            Employee employee = employeeRepository.findById(employeeId)
                    .orElseThrow(() -> new IllegalArgumentException("员工不存在"));
            employee.setState(context.getState().getId());
            employeeRepository.save(employee);
        }

        @Override
        public StateMachineContext<EmployeeState, EmployeeEvent> read(String employeeIdStr) throws Exception {
            // 状态机启动时,从数据库读取员工当前状态,恢复上下文
            Long employeeId = Long.parseLong(employeeIdStr);
            Employee employee = employeeRepository.findById(employeeId)
                    .orElseThrow(() -> new IllegalArgumentException("员工不存在"));
            return new DefaultStateMachineContext<>(employee.getState(), null, null, null);
        }
    };
}

// 添加持久化拦截器,自动处理状态变化时的持久化
@Bean
public PersistStateMachineInterceptor<EmployeeState, EmployeeEvent, String> persistInterceptor() {
    return new PersistStateMachineInterceptor<>(stateMachinePersist());
}

然后在configure方法中注册这个拦截器:

@Override
public void configure(StateMachineConfigurationConfigurer<EmployeeState, EmployeeEvent> config) throws Exception {
    config
        .withConfiguration()
        .autoStartup(true)
        .listener(listener())
        .stateMachineCache(stateMachineCache())
        .and()
        .withPersistence()
        .persist(stateMachinePersist())
        .restore(true)
        .and()
        .withInterceptors(persistInterceptor()); // 注册持久化拦截器
}

3. 简化状态机构建方法

现在缓存和持久化都配置好了,你的build方法可以大幅简化,不需要手动重置状态机:

private StateMachine<EmployeeState, EmployeeEvent> build(Long employeeId){
    String machineId = Long.toString(employeeId);
    // 从工厂获取:缓存存在则返回已有实例,不存在则创建新实例并自动恢复状态
    StateMachine<EmployeeState, EmployeeEvent> stateMachine = stateMachineFactory.getStateMachine(machineId);
    
    // 确保状态机处于运行状态(autoStartup为true,但缓存的实例可能已启动)
    if (!stateMachine.isRunning()) {
        stateMachine.start();
    }
    
    return stateMachine;
}

测试验证

修改你的测试用例,现在应该能通过:

@Test
public void sameEmployeeIdTest() {
    String employeeId = "1";
    // 第一次获取并发送事件
    StateMachine<EmployeeState, EmployeeEvent> stateMachine = factory.getStateMachine(employeeId);
    stateMachine.sendEvent(EmployeeEvent.CHECKSTARTED);
    assertEquals(EmployeeState.INCHECK, stateMachine.getState().getId());
    
    // 第二次获取同一个员工ID的状态机
    StateMachine<EmployeeState, EmployeeEvent> stateMachine2 = factory.getStateMachine(employeeId);
    assertEquals(stateMachine.getUuid(), stateMachine2.getUuid()); // 验证是同一个实例
    assertEquals(EmployeeState.INCHECK, stateMachine2.getState().getId()); // 验证状态一致
}

额外注意事项

  1. 内存泄漏防范:如果员工被删除,记得从StateMachineCache中移除对应的实例,避免内存占用;
  2. 状态枚举一致性:确保员工实体中存储的状态字符串和EmployeeState枚举完全匹配,避免恢复状态时出错;
  3. 并发安全:如果多个线程同时操作同一个员工的状态机,建议添加同步机制,或者利用Spring State Machine的线程安全特性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.30 17:13:12