Spring Batch中如何为聚合输出文件生成含记录数的页脚
针对你在Spring Batch里的这个需求——处理两个输入文件去重后聚合为单个输出,还要在输出末尾加包含总写入记录数的页脚,我整理了一套可行的实现方案,分几个关键环节来做:
实现方案
1. 跨步骤共享记录计数器
因为你是用单独步骤处理每个输入文件,所以需要一个能在多个步骤之间共享的计数器来统计总写入记录数。这里我们用JobExecutionContext来存储,它是整个Job级别的上下文,能跨步骤传递数据。
为了避免多线程步骤下的并发问题,建议用AtomicInteger作为计数器类型,在Job启动时初始化:
@Bean public Job aggregateFilesJob(JobBuilderFactory jobBuilderFactory, Step processFirstFileStep, Step processSecondFileStep, Step writeFooterStep) { return jobBuilderFactory.get("aggregateFilesJob") .listener(new JobExecutionListener() { @Override public void beforeJob(JobExecution jobExecution) { // 初始化原子计数器,保证线程安全 jobExecution.getExecutionContext().put("totalWritten", new AtomicInteger(0)); } }) .start(processFirstFileStep) .next(processSecondFileStep) .next(writeFooterStep) .build(); }
2. 每个输入文件的处理步骤(去重+写入+计数)
每个步骤需要完成三个事情:读取输入文件、过滤重复记录、写入输出文件,同时累加写入的记录数。
2.1 去重处理器
用ItemProcessor实现去重逻辑,这里用线程安全的Set来存储已处理过的唯一标识,避免重复记录流入Writer:
@Bean public ItemProcessor<YourDataDto, YourDataDto> deduplicationProcessor() { // 用ConcurrentHashMap的keySet保证线程安全,适合多线程步骤 Set<String> processedUniqueKeys = ConcurrentHashMap.newKeySet(); return item -> { // 假设你的数据对象有一个唯一标识字段,比如id String uniqueKey = item.getId(); // add方法返回false表示已存在,返回null会被Writer过滤掉 if (processedUniqueKeys.add(uniqueKey)) { return item; } return null; }; }
2.2 带计数监听的Writer
配置FlatFileItemWriter时,开启追加模式(让第二个步骤的写入不会覆盖第一个步骤的内容),同时注册ItemWriteListener来累加计数器:
@Bean public FlatFileItemWriter<YourDataDto> outputFileWriter() { FlatFileItemWriter<YourDataDto> writer = new FlatFileItemWriter<>(); writer.setResource(new FileSystemResource("final-output.csv")); writer.setAppendAllowed(true); // 关键:第二个步骤追加写入同一文件 writer.setLineAggregator(new DelimitedLineAggregator<>() {{ setDelimiter(","); setFieldExtractor(new BeanWrapperFieldExtractor<>() {{ // 替换成你的数据对象字段名 setNames(new String[]{"id", "name", "value"}); }}); }}); // 注册监听,累加写入记录数 writer.registerListener(new ItemWriteListener<YourDataDto>() { @Override public void afterWrite(List<? extends YourDataDto> items) { JobExecution jobExecution = StepSynchronizationManager.getContext().getJobExecution(); ExecutionContext jobContext = jobExecution.getExecutionContext(); AtomicInteger totalWritten = (AtomicInteger) jobContext.get("totalWritten"); // 原子性累加当前批次写入的记录数 totalWritten.addAndGet(items.size()); } }); return writer; }
两个输入文件的处理步骤可以复用这个Writer(因为开启了追加),只需要各自配置对应的ItemReader即可。
3. 生成页脚的单独步骤
在两个文件处理完成后,添加一个专门的Tasklet步骤,从JobExecutionContext中取出总记录数,写入页脚到输出文件:
@Bean public Tasklet footerWritingTasklet() { return (contribution, chunkContext) -> { JobExecution jobExecution = chunkContext.getStepContext().getJobExecution(); AtomicInteger totalWritten = (AtomicInteger) jobExecution.getExecutionContext().get("totalWritten"); int total = totalWritten != null ? totalWritten.get() : 0; // 写入页脚到输出文件,注意开启追加模式 try (FileWriter fileWriter = new FileWriter("final-output.csv", true)) { // 自定义页脚格式,比如"--- Total Records: X ---" fileWriter.write(String.format("%n--- Total Records Written: %d ---%n", total)); } catch (IOException e) { throw new JobExecutionException("Failed to write footer record", e); } return RepeatStatus.FINISHED; }; } // 配置页脚步骤 @Bean public Step writeFooterStep(StepBuilderFactory stepBuilderFactory) { return stepBuilderFactory.get("writeFooterStep") .tasklet(footerWritingTasklet()) .build(); }
关键注意事项
- 如果你的步骤是多线程(比如用
TaskExecutor),一定要用线程安全的计数器(AtomicInteger)和去重集合(ConcurrentHashMap.newKeySet()),避免并发问题。 - 确保
FlatFileItemWriter的appendAllowed设置为true,否则第二个步骤会覆盖第一个步骤的输出内容。 - 如果需要更复杂的页脚格式,可以自定义
FlatFileFooterCallback,但因为我们是跨步骤写入,单独用Tasklet写页脚会更灵活。
内容的提问来源于stack exchange,提问作者Tim Grunewald
相关产品推荐
相关产品推荐

