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

Spring Batch定时任务仅首次执行有效,后续仅显示完成无读写

Spring Batch定时任务仅首次执行读写的问题分析与解决

你遇到的问题很典型——定时调度的Spring Batch任务每次都能触发并标记为COMPLETED,但只有第一次有实际的REST读取、处理和写入操作,后续任务直接跳过了核心步骤。结合你的代码和batch_job_execution表数据,我们来一步步排查:

从数据库数据看异常

先看你的batch_job_execution记录:

  • 第一条任务(ID338):开始时间2018-05-18 14:36:36,结束时间2018-05-18 14:37:47,有明显的执行耗时,说明确实跑了读写逻辑。
  • 后续任务(ID339及以后):开始和结束时间完全一致,几乎瞬间完成,说明任务没有执行任何实际的步骤处理,直接标记为完成。

你的代码细节

Spring Boot应用入口

@EnableScheduling
@SpringBootApplication
public class BatchApplication {
    public static void main(String[] args) {
        SpringApplication.run(BatchApplication.class,args);
    }
}

任务调度器

@Component
public class BatchScheduler {
    Logger log = Logger.getLogger(BatchScheduler.class);
    @Autowired
    private JobLauncher jobLauncher;
    @Autowired
    private JobCompletionNotificationListener listener;
    @Autowired
    private Step step1;
    @Autowired
    public JobBuilderFactory jobBuilderFactory;
    @Autowired
    private CreateRegistrationsBatchConfiguration job;

    @Scheduled(fixedRate = 85000)
    public void runJob() {
        try{
            JobExecution execution = jobLauncher.run(
                job.importRegistrationJob(jobBuilderFactory, listener, step1),
                new JobParametersBuilder().addLong("uniqueness", System.nanoTime()).toJobParameters()
            );
            log.info("Job finished with status :" + execution.getStatus());
        }catch(Exception exception) {
            log.error(exception.getMessage());
        }
    }
}

Spring Batch配置类

@Configuration
@EnableBatchProcessing
public class CreateRegistrationsBatchConfiguration {
    Logger log = Logger.getLogger(CreateRegistrationsBatchConfiguration.class);
    @Autowired
    public JobBuilderFactory jobBuilderFactory;
    @Autowired
    public StepBuilderFactory stepBuilderFactory;
    @Autowired
    private Environment environment;

    @Bean
    RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @Bean
    ItemReader<EmployeeEmploymentDTO> restEmployeeReader(Environment environment, RestTemplate restTemplate) {
        return new RESTEmployeeReader(
            environment.getRequiredProperty("rest.api.url"),
            restTemplate
        );
    }

    @Bean
    public RegistrationItemProcessor processor() {
        return new RegistrationItemProcessor();
    }

    @Bean
    public ItemWriter<List<Registration>> writer() {
        return new MultiOutputItemWriter();
    }

    @Bean
    public Job importRegistrationJob(JobBuilderFactory jobs, JobCompletionNotificationListener listener, Step step1) {
        return jobBuilderFactory.get("importRegistrationJob")
            .incrementer(new RunIdIncrementer())
            .listener(listener)
            .flow(step1)
            .end()
            .build();
    }

    @Bean
    public Step step1(StepBuilderFactory stepBuilderFactory, ItemReader<EmployeeEmploymentDTO> reader, ItemWriter<List<Registration>> writer, ItemProcessor<EmployeeEmploymentDTO, List<Registration>> processor) {
        return stepBuilderFactory.get("step1").allowStartIfComplete(true)
            .<EmployeeEmploymentDTO, List<Registration>>chunk(10)
            .reader(restEmployeeReader(environment,restTemplate()))
            .processor(processor())
            .writer(writer)
            .build();
    }
}

问题根源

最可能的原因是你的RESTEmployeeReader是单例Bean,且没有重置读取状态:

  • Spring默认所有Bean都是单例模式,所以第一次Job执行时,Reader读取完数据后,内部的状态(比如是否已经读取完毕的标记、游标位置等)会保持在“已读完”的状态。
  • 后续Job执行时,复用同一个Reader实例,hasNext()直接返回false,导致整个Step没有任何数据处理,直接结束。

另外,你在Step配置中加了allowStartIfComplete(true),这个配置是允许Step在已经完成后再次启动,但它不会自动重置Reader的状态。

解决方案

方案1:将Reader设置为原型Scope

让Spring每次创建Job实例时都生成新的Reader,避免状态复用:

@Bean
@Scope("prototype") // 关键:每次请求都创建新实例
ItemReader<EmployeeEmploymentDTO> restEmployeeReader(Environment environment, RestTemplate restTemplate) {
    return new RESTEmployeeReader(
        environment.getRequiredProperty("rest.api.url"),
        restTemplate
    );
}

方案2:让Reader实现ItemStream接口重置状态

如果不想改变Bean的Scope,可以让RESTEmployeeReader实现ItemStream接口,在每次Job启动时重置读取状态:

public class RESTEmployeeReader implements ItemReader<EmployeeEmploymentDTO>, ItemStream {
    private String apiUrl;
    private RestTemplate restTemplate;
    private List<EmployeeEmploymentDTO> dataList;
    private int currentIndex = 0;

    // 构造方法省略

    @Override
    public EmployeeEmploymentDTO read() throws Exception {
        if (dataList == null) {
            dataList = restTemplate.getForObject(apiUrl, new ParameterizedTypeReference<List<EmployeeEmploymentDTO>>() {});
        }
        if (currentIndex < dataList.size()) {
            return dataList.get(currentIndex++);
        } else {
            return null; // 表示读取完毕
        }
    }

    @Override
    public void open(ExecutionContext executionContext) throws ItemStreamException {
        // 每次Job启动时重置状态
        currentIndex = 0;
        dataList = null;
    }

    @Override
    public void update(ExecutionContext executionContext) throws ItemStreamException {
        // 可选:保存当前状态到ExecutionContext,用于重启
        executionContext.putInt("currentIndex", currentIndex);
    }

    @Override
    public void close() throws ItemStreamException {
        // 清理资源
    }
}

额外检查点

  • 确认你的REST接口在后续调用时确实能返回数据(可以单独测试接口),排除接口本身返回空的情况。
  • 检查JobParameters是否正确生成:你用了System.nanoTime()作为唯一参数,这部分是对的,确保每次Job都是新的实例,不会被Spring Batch认为是重复任务。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 04:12:43