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

Spring State Machine JPA配置:无MachineId创建状态机实例报错求助

问题描述

我需要实现:当数据库中不存在指定ID的状态机实例时,自动创建新实例并将配置(状态、过渡)和上下文存储到数据库。但目前遇到两个问题:

  1. 无法在数据库中设置默认配置,插入过渡记录时不指定machineId,创建新状态机时会抛出**"Must have at least one transition"**异常。
  2. 数据库中已存在未指定machineId的过渡记录,希望在运行时创建带随机machineId的状态机,但代码里定义过渡无需指定machineId,数据库里这么做却无法正常工作。

现有配置代码

@Configuration
@RequiredArgsConstructor
@EnableStateMachine
public class StateMachineConfig {

    @Bean
    public StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister(
            JpaStateMachineRepository jpaStateMachineRepository) {
        return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
    }

    @Bean
    public StateMachineService<String, String> stateMachineService(
            StateMachineFactory<String, String> stateMachineFactory,
            StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister) {
        return new DefaultStateMachineService<>(stateMachineFactory, stateMachineRuntimePersister);
    }

    @Bean
    public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
        StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
        factoryBean.setResources(new Resource[]{new ClassPathResource("data.json")});
        return factoryBean;
    }

    @Configuration
    @EnableStateMachineFactory
    public static class Config extends StateMachineConfigurerAdapter<String, String> {

        @Autowired
        private StateRepository<? extends RepositoryState> stateRepository;

        @Autowired
        private TransitionRepository<? extends RepositoryTransition> transitionRepository;

        @Autowired
        private StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister;

        @Override
        public void configure(StateMachineConfigurationConfigurer<String, String> config)
                throws Exception {
            config
                    .withPersistence()
                    .runtimePersister(stateMachineRuntimePersister);
        }

        @Override
        public void configure(StateMachineModelConfigurer<String, String> model)
                throws Exception {
            model
                    .withModel()
                    .factory(modelFactory());
        }

        @Bean
        public StateMachineModelFactory<String, String> modelFactory() {
            return new RepositoryStateMachineModelFactory(stateRepository, transitionRepository);
        }
    }
}

现有控制器代码

@RequestMapping("/state")
public String feedAndGetStates(
        @RequestParam(value = "events", required = false) List<String> events,
        @RequestParam(value = "machine", required = false, defaultValue = MACHINE_ID_1) String machine,
        Model model) throws Exception {

    StateMachine<String, String> stateMachine = getStateMachine(machine);
    if (events != null) {
        for (String event : events) {
            stateMachine
                    .sendEvent(Mono.just(MessageBuilder
                            .withPayload(event).build()))
                    .blockLast();
        }
    }

    StringBuilder contextBuf = new StringBuilder();

    StateMachineContext<String, String> stateMachineContext = stateMachinePersist.read(machine);
    if (stateMachineContext != null) {
        contextBuf.append(stateMachineContext.toString());
    }
    if (ObjectUtils.nullSafeEquals(machine, MACHINE_ID_2)) {
        stateMachineContext = stateMachinePersist.read(MACHINE_ID_2R1);
        if (stateMachineContext != null) {
            contextBuf.append("\n---\n");
            contextBuf.append(stateMachineContext.toString());
        }
        stateMachineContext = stateMachinePersist.read(MACHINE_ID_2R2);
        if (stateMachineContext != null) {
            contextBuf.append("\n---\n");
            contextBuf.append(stateMachineContext.toString());
        }
    }

    model.addAttribute("allMachines", MACHINES);
    model.addAttribute("machine", machine);
    model.addAttribute("currentMachine", currentStateMachine);
    model.addAttribute("allEvents", getEvents());
    model.addAttribute("messages", createMessages(listener.getMessages()));
    model.addAttribute("context", contextBuf.toString());
    return "states";
}

private synchronized StateMachine<String, String> getStateMachine(String machineId) throws Exception {
    listener.resetMessages();
    if (currentStateMachine == null) {
        currentStateMachine = stateMachineService.acquireStateMachine(machineId, false);
        currentStateMachine.addStateListener(listener);
        currentStateMachine.startReactively().block();
    } else if (!ObjectUtils.nullSafeEquals(currentStateMachine.getId(), machineId)) {
        stateMachineService.releaseStateMachine(currentStateMachine.getId());
        currentStateMachine.stopReactively().block();
        currentStateMachine = stateMachineService.acquireStateMachine(machineId, false);
        currentStateMachine.addStateListener(listener);
        currentStateMachine.startReactively().block();
    }
    return currentStateMachine;
}
解决方案

问题核心是默认的RepositoryStateMachineModelFactory会严格根据machineId过滤数据库中的状态和过渡记录,不带machineId的模板记录不会被加载,导致新状态机没有过渡配置而抛出异常。以下是具体解决步骤:

1. 自定义状态机模型工厂,支持模板复用

继承RepositoryStateMachineModelFactory,修改查询逻辑:当指定machineId没有对应记录时,加载machineId为null的模板记录,复制并保存为新machineId的实例。

public class CustomRepositoryStateMachineModelFactory extends RepositoryStateMachineModelFactory<String, String> {

    private final StateRepository<? extends RepositoryState> stateRepository;
    private final TransitionRepository<? extends RepositoryTransition> transitionRepository;

    public CustomRepositoryStateMachineModelFactory(StateRepository<? extends RepositoryState> stateRepository,
                                                   TransitionRepository<? extends RepositoryTransition> transitionRepository) {
        super(stateRepository, transitionRepository);
        this.stateRepository = stateRepository;
        this.transitionRepository = transitionRepository;
    }

    @Override
    public StateMachineModel<String, String> build(String machineId) {
        // 先查询当前machineId的状态和过渡
        List<? extends RepositoryState> states = stateRepository.findByMachineId(machineId);
        List<? extends RepositoryTransition> transitions = transitionRepository.findByMachineId(machineId);

        // 如果没有记录,加载模板(machineId为null)
        if (states.isEmpty() && transitions.isEmpty()) {
            states = stateRepository.findByMachineIdIsNull();
            transitions = transitionRepository.findByMachineIdIsNull();

            // 存在模板时,复制为当前machineId的实例并存库
            if (!states.isEmpty() && !transitions.isEmpty()) {
                saveTemplateAsMachineInstance(machineId, states, transitions);
            }
        }

        return super.build(machineId);
    }

    private void saveTemplateAsMachineInstance(String machineId, List<? extends RepositoryState> templateStates, List<? extends RepositoryTransition> templateTransitions) {
        // 复制状态并关联新machineId
        Map<String, RepositoryState> newStateMap = new HashMap<>();
        templateStates.forEach(template -> {
            RepositoryState newState = new RepositoryState();
            // 复制模板属性,可使用BeanUtils.copyProperties(template, newState)
            newState.setState(template.getState());
            newState.setInitial(template.isInitial());
            newState.setMachineId(machineId);
            newState = stateRepository.save(newState);
            newStateMap.put(template.getState(), newState);
        });

        // 复制过渡并关联新machineId和对应状态
        templateTransitions.forEach(template -> {
            RepositoryTransition newTransition = new RepositoryTransition();
            // 复制模板属性
            newTransition.setEvent(template.getEvent());
            newTransition.setMachineId(machineId);
            // 关联新创建的源状态和目标状态
            newTransition.setSource(newStateMap.get(template.getSource().getState()));
            newTransition.setTarget(newStateMap.get(template.getTarget().getState()));
            transitionRepository.save(newTransition);
        });
    }
}

2. 替换配置中的模型工厂

修改StateMachineConfig.Config中的modelFactory Bean,使用自定义工厂:

@Bean
public StateMachineModelFactory<String, String> modelFactory() {
    return new CustomRepositoryStateMachineModelFactory(stateRepository, transitionRepository);
}

3. 调整状态机获取逻辑,支持随机ID生成

修改getStateMachine方法,支持生成随机machineId并触发模板复制:

private synchronized StateMachine<String, String> getStateMachine(String machineId) throws Exception {
    listener.resetMessages();
    // 若传入标识为"new",生成随机UUID作为machineId
    if ("new".equals(machineId)) {
        machineId = UUID.randomUUID().toString();
        // 提前触发模型构建,确保模板被复制到新ID下
        stateMachineFactory.getStateMachine(machineId);
    }

    if (currentStateMachine == null) {
        currentStateMachine = stateMachineService.acquireStateMachine(machineId, false);
        currentStateMachine.addStateListener(listener);
        currentStateMachine.startReactively().block();
    } else if (!ObjectUtils.nullSafeEquals(currentStateMachine.getId(), machineId)) {
        stateMachineService.releaseStateMachine(currentStateMachine.getId());
        currentStateMachine.stopReactively().block();
        currentStateMachine = stateMachineService.acquireStateMachine(machineId, false);
        currentStateMachine.addStateListener(listener);
        currentStateMachine.startReactively().block();
    }
    return currentStateMachine;
}

4. 确保数据库实体支持模板记录

修改RepositoryState和RepositoryTransition实体,允许machineId为null:

@Entity
public class RepositoryState {
    // 其他字段(如id、state、initial等)
    @Column(name = "machine_id", nullable = true)
    private String machineId;
    // getter、setter方法
}

@Entity
public class RepositoryTransition {
    // 其他字段(如id、event等)
    @Column(name = "machine_id", nullable = true)
    private String machineId;
    // 关联状态字段
    @ManyToOne
    private RepositoryState source;
    @ManyToOne
    private RepositoryState target;
    // getter、setter方法
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 13:52:13