
本文详解如何在 Spring Batch 中构建支持自定义 Reader(按 ID 批量拉取)、Processor(返回 List 对象)和 Writer(批量持久化)的作业流程,并重点解决因错误使用 @StepScope 导致的状态丢失与重复写入问题。
本文详解如何在 spring batch 中构建支持自定义 reader(按 id 批量拉取)、processor(返回 list 对象)和 writer(批量持久化)的作业流程,并重点解决因错误使用 `@stepscope` 导致的状态丢失与重复写入问题。
在 Spring Batch 的典型流式处理中,Reader → Processor → Writer 的数据契约通常是 1:1:1(即每次 read() 返回一个 item,process() 输出一个 item,write() 接收一个 item 列表)。但实际业务中常需突破该范式——例如:Reader 预加载主键列表、Processor 根据单个 ID 查询并组装多个关联实体、Writer 批量保存整个子集合。本文将指导你安全、高效地实现此类“1 → N → N”映射流程,并规避常见陷阱。
✅ 正确的作用域配置是核心前提
你当前代码中对 CustomItemReader 使用 @StepScope 是根本性错误。@StepScope 会在每一步执行前重新创建 Bean 实例,导致 bookingIds 字段在每次 read() 调用时都被重置为 null,进而触发重复查询与状态丢失——这正是你观察到“chunk 数据不断累积、重复插入”的根源。
✅ 正确做法如下:
- Reader 必须使用 @JobScope:确保在整个 Job 生命周期内仅初始化一次,bookingIds 可被缓存并逐个消费;
- Processor 和 Writer 应移除 @StepScope:它们不依赖 Step 级别参数,作为无状态单例(Spring 默认作用域)完全足够;
- 启用 @JobScope 支持:在任意 @Configuration 类上添加 @EnableBatchProcessing(Spring Boot 2.4+ 已自动启用,但仍建议显式确认)。
@Component
@JobScope // ✅ 关键修改:改为 @JobScope
public class CustomItemReader implements ItemReader<String> {
@Autowired
private BookingInfoRepository bookingInfoRepository;
private List<String> bookingIds; // 懒加载,仅初始化一次
@Override
public String read() {
if (bookingIds == null) {
bookingIds = bookingInfoRepository.findDistinctId();
}
return bookingIds.isEmpty() ? null : bookingIds.remove(0);
}
}⚠️ 注意:@JobScope Bean 无法直接注入 @Autowired 的非 @JobScope Bean(如 BookingInfoRepository),但 Spring Batch 会通过代理机制自动处理依赖注入,无需额外配置。
✅ Processor 与 Writer 的契约适配
你的 CorrectionProcessor 返回 List<BookingInfo> 是合理的设计,但需明确:Spring Batch 的 ItemProcessor<I, O> 契约允许 O 为任意类型(包括 List),只要 Writer 能匹配即可。关键在于 Writer 必须能正确解包并处理该嵌套结构。
你当前的 CustomItemWriter 存在两处隐患:
- write() 方法签名接收 Chunk<? extends List<BookingInfo>> —— 这意味着每个 chunk 元素本身就是一个 List<BookingInfo>,而 Chunk 是 Spring Batch 封装的分块集合(如 Chunk<List<BookingInfo>>);
- 内层循环 for(List<BookingInfo> bookingInfo : chunk) 实际遍历的是每个 List<BookingInfo>,而非单个 BookingInfo,因此应调用 bookingInfoRepository.saveAll(...) 而非 save(...)。
修正后的 Writer 示例:
@Component // ✅ 移除 @StepScope
public class CustomItemWriter implements ItemWriter<List<BookingInfo>> {
@Autowired
private BookingInfoRepository bookingInfoRepository;
@Override
public void write(Chunk<? extends List<BookingInfo>> chunk) throws Exception {
// chunk 包含多个 List<BookingInfo>,每个来自一次 process()
for (List<BookingInfo> bookingInfoList : chunk) {
if (!bookingInfoList.isEmpty()) {
bookingInfoRepository.saveAll(bookingInfoList); // ✅ 批量保存
}
}
}
}✅ 完整流程验证与最佳实践建议
- Reader 状态一致性:@JobScope + 懒加载 bookingIds 确保 ID 列表只查一次,且顺序消费,避免并发或重启导致的重复/遗漏;
- Processor 无状态性:String → List<BookingInfo> 的转换逻辑应幂等,不依赖外部可变状态;
- Writer 幂等与事务:saveAll() 在同一事务内执行,若需更强一致性,可在 Step 配置中启用 @Transactional 或设置 JpaItemWriter 的 entityManagerFactory;
- 性能优化提示:若 bookingIds 数量极大,可考虑改用 JdbcCursorItemReader 或分页查询,避免内存溢出;
- 异常与重试:在 process() 或 write() 中抛出异常将触发 Spring Batch 的重试/跳过机制,务必结合 FaultTolerantStepBuilder 显式配置。
综上,通过精准控制 Bean 作用域、严格匹配数据契约、并遵循 Spring Batch 的生命周期语义,你不仅能安全实现“ID → 多对象 → 批量落库”的定制流程,还能获得框架原生的监控、重启、事务保障等企业级能力。

















