
本文详解如何在 Spring Batch 中构建支持自定义读取器、处理器和写入器的批处理流程,重点解决因误用 @StepScope 导致的数据重复写入问题,并提供符合批处理语义的 List 处理方案。
本文详解如何在 spring batch 中构建支持自定义读取器、处理器和写入器的批处理流程,重点解决因误用 `@stepscope` 导致的数据重复写入问题,并提供符合批处理语义的 `list
在 Spring Batch 中实现“单 ID 输入 → 多对象输出 → 批量持久化”的定制化流程时,核心挑战在于组件生命周期管理与数据流语义一致性。您当前的实现存在两个关键设计偏差:一是 @StepScope 被错误应用于 ItemReader,导致每次 read() 调用前实例被重建,bookingIds 清空重置;二是 ItemProcessor
✅ 正确的组件作用域配置
@Component
@JobScope // ✅ 改为 @JobScope:确保整个 Job 生命周期内复用同一 Reader 实例
public class CustomItemReader implements ItemReader<string> {
@Autowired
private BookingInfoRepository bookingInfoRepository;
private Iterator<string> idIterator; // 使用 Iterator 替代 List.remove(0),更高效且线程安全
@Override
public String read() {
if (idIterator == null) {
List<string> ids = bookingInfoRepository.findDistinctId();
idIterator = ids.iterator();
}
return idIterator.hasNext() ? idIterator.next() : null;
}
}</string></string></string>
- @JobScope 保证 Reader 在 Job 启动时初始化一次,状态(如 idIterator)全程保持;
- 移除 @StepScope(它适用于需按 Step 参数动态注入的场景,如 @Value("#{jobParameters['inputFile']}"));
- @EnableBatchProcessing 必须在主配置类上声明,否则 @JobScope 不生效:
@Configuration
@EnableBatchProcessing
public class BatchConfig {
// 其他 Bean 定义...
}
CorrectionProcessor 和 CustomItemWriter 应移除所有 Scope 注解,使用默认 @Component(即 singleton)即可:
@Component // ✅ 默认 singleton,无需额外注解
@Slf4j
public class CorrectionProcessor implements ItemProcessor<string bookinginfo> {
@Autowired
private BookingInfoRepository bookingInfoRepository;
@Override
public BookingInfo process(String bookingId) throws Exception {
// ❌ 错误:返回 List<bookinginfo> 破坏 Chunk 语义
// ✅ 正确:每次仅处理并返回一个 BookingInfo(可循环调用)
List<bookinginfo> list = bookingInfoRepository.findById(bookingId);
return list.stream()
.peek(info -> {
// 在此处对单个 BookingInfo 做业务修正
info.setProcessedAt(LocalDateTime.now());
})
.findFirst()
.orElse(null);
}
}</bookinginfo></bookinginfo></string>
⚠️ 重要原则:Spring Batch 的 Chunk-oriented processing 要求 Reader → Processor → Writer 链中每个环节处理一对一映射。若一个 bookingId 对应多个 BookingInfo,应在 Reader 层完成展开(见下文替代方案),或改用 TaskletStep。
✅ 替代方案:在 Reader 中展开多对象(推荐)
若业务逻辑强制要求“1 ID → N 对象”,最佳实践是将展开逻辑前置到 ItemReader:
@Component
@JobScope
public class BookingIdToBookingInfoReader implements ItemReader<bookinginfo> {
@Autowired
private BookingInfoRepository bookingInfoRepository;
private Iterator<bookinginfo> infoIterator;
@Override
public BookingInfo read() {
if (infoIterator == null) {
List<string> ids = bookingInfoRepository.findDistinctId();
List<bookinginfo> allInfos = new ArrayList();
for (String id : ids) {
allInfos.addAll(bookingInfoRepository.findById(id));
}
infoIterator = allInfos.iterator();
}
return infoIterator.hasNext() ? infoIterator.next() : null;
}
}</bookinginfo></string></bookinginfo></bookinginfo>
此时 Processor 和 Writer 恢复标准签名:
@Component
public class BookingInfoProcessor implements ItemProcessor<bookinginfo bookinginfo> {
@Override
public BookingInfo process(BookingInfo item) {
// 修改单个 BookingInfo
return item;
}
}
@Component
public class BookingInfoWriter implements ItemWriter<bookinginfo> {
@Autowired
private BookingInfoRepository repository;
@Override
public void write(Chunk extends BookingInfo> chunk) throws Exception {
repository.saveAll(chunk.getItems()); // ✅ 批量保存,高效且事务安全
}
}</bookinginfo></bookinginfo>
? 关于重复写入的根本原因
您观察到“chunk 数据不断追加”实为 @StepScope + List.remove(0) 的副作用:
- 每次 read() 被调用时,CustomItemReader 被重新创建;
- bookingIds 初始化为空,触发 findDistinctId() 重新查询全量 ID;
- remove(0) 从新列表取第一个,但因实例不复用,无法推进指针 → 永远返回首 ID;
- 导致无限循环处理同一 ID,Processor 反复生成相同 List
,Writer 持续插入重复数据。
✅ 总结:关键修复清单
| 组件 | 错误做法 | 正确做法 | 原因 |
|---|---|---|---|
| ItemReader | @StepScope | @JobScope + Iterator | 保持读取状态,避免重复查询 |
| ItemProcessor | 返回 List |
返回单个 T | 遵循 Chunk 模型,保障事务粒度与可重试性 |
| ItemWriter | 循环 save() 单条 | repository.saveAll(chunk.getItems()) | 利用 JPA 批量操作,提升性能并避免 N+1 问题 |
| 配置类 | 缺少 @EnableBatchProcessing | 显式添加该注解 | 启用 @JobScope / @StepScope 支持 |
通过以上调整,您的流程将严格遵循 Spring Batch 的设计哲学:状态可控、语义清晰、性能可靠,彻底规避数据重复与状态丢失风险。











