Spring StateMachineFactory获取状态机始终返回初始状态:如何为每个员工维护独立状态机实例
解决Spring State Machine为每个员工维护独立实例的问题
看起来你碰到的核心问题是:无法让同一个员工对应的状态机实例保持状态一致性——要么每次获取到新实例(状态重置为初始),要么实例是同一个但状态没保留。下面我会一步步拆解问题根源,给出具体的解决方案:
问题根源分析
先看你测试用例的结果:
- 用
UUID作为ID时,两次获取的是同一个实例(UUID相同),但状态不同——这是因为状态机的状态没有被持久化,且你没有在获取时恢复状态; - 用字符串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()); // 验证状态一致 }
额外注意事项
- 内存泄漏防范:如果员工被删除,记得从
StateMachineCache中移除对应的实例,避免内存占用; - 状态枚举一致性:确保员工实体中存储的状态字符串和
EmployeeState枚举完全匹配,避免恢复状态时出错; - 并发安全:如果多个线程同时操作同一个员工的状态机,建议添加同步机制,或者利用Spring State Machine的线程安全特性。
内容的提问来源于stack exchange,提问作者Yunus
相关产品推荐
相关产品推荐

