Spring Batch技术问题:ClassifierCompositeItemWriter的Footer回调未触发执行
我完全懂你碰到的这个问题——用ClassifierCompositeItemWriter按分支ID生成独立文件时,页眉能正常输出,但页脚死活不出现。核心问题出在你手动接管了子FlatFileItemWriter的生命周期,导致这些writer的close()方法没被Spring Batch调用,而页脚回调恰恰是在close()阶段执行的。另外,你用实例变量HashMap存储writer的方式在@StepScope下也有隐患,因为StepScope的bean每次step执行都会生成新实例,实例变量没法跨chunk保留状态。
解决方案核心思路
- 放弃手动调用
open():让Spring Batch统一管理writer的生命周期,ClassifierCompositeItemWriter会在自身的open()/close()阶段,自动调用所有子writer的对应方法,这才是触发页脚的关键。 - 用
ExecutionContext存储writer:替换原来的实例变量HashMap,ExecutionContext是Step执行的持久化上下文,能安全跨chunk保存writer实例,符合Spring Batch的状态管理规范。 - 移除冗余配置:不需要手动设置
appendAllowed=true,Spring Batch会自动处理文件初始化和后续追加逻辑。
修改后的完整代码
import java.util.Arrays; import java.util.HashMap; import java.util.Map; import org.springframework.batch.core.Job; import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; import org.springframework.batch.core.configuration.annotation.StepScope; import org.springframework.batch.core.launch.JobLauncher; import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.item.ItemReader; import org.springframework.batch.item.ItemWriter; import org.springframework.batch.item.file.FlatFileItemWriter; import org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor; import org.springframework.batch.item.file.transform.DelimitedLineAggregator; import org.springframework.batch.item.support.ClassifierCompositeItemWriter; import org.springframework.batch.item.support.ListItemReader; import org.springframework.classify.Classifier; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.FileSystemResource; @Configuration @EnableBatchProcessing public class MyJob3 { private static final String BRANCH_WRITERS_KEY = "branchWriters"; public static void main(String[] args) throws Exception { ApplicationContext context = new AnnotationConfigApplicationContext(MyJob3.class); JobLauncher jobLauncher = context.getBean(JobLauncher.class); Job job = context.getBean(Job.class); jobLauncher.run(job, new JobParameters()); } @Bean public Job job(JobBuilderFactory jobs, StepBuilderFactory steps) { return jobs.get("job") .start(steps.get("step") .<Student, Student>chunk(5) .reader(itemReader()) .writer(classifierCompositeItemWriter()) .build()) .build(); } @Bean @StepScope public ItemReader<Student> itemReader() { Student A = new Student("A", 1); Student B = new Student("B", 2); Student C = new Student("C", 1); Student D = new Student("D", 4); Student E = new Student("E", 4); return new ListItemReader<>(Arrays.asList(A,B,C,D,E)); } @Bean @StepScope public ClassifierCompositeItemWriter<Student> classifierCompositeItemWriter() { ClassifierCompositeItemWriter<Student> compositeWriter = new ClassifierCompositeItemWriter<>(); compositeWriter.setClassifier(itemWriterClassifier(null)); // 这里传null,Spring会自动注入StepExecution return compositeWriter; } @Bean @StepScope public Classifier<Student, ItemWriter<? super Student>> itemWriterClassifier(StepExecution stepExecution) { return student -> { ExecutionContext executionContext = stepExecution.getExecutionContext(); // 从ExecutionContext获取已创建的writer映射,不存在则初始化 Map<Integer, FlatFileItemWriter<Student>> writers = (Map<Integer, FlatFileItemWriter<Student>>) executionContext.get(BRANCH_WRITERS_KEY); if (writers == null) { writers = new HashMap<>(); executionContext.put(BRANCH_WRITERS_KEY, writers); } Integer branchId = student.getBranchId(); // 复用已存在的writer if (writers.containsKey(branchId)) { return writers.get(branchId); } // 创建新的FlatFileItemWriter String fileName = "Branch_Info_" + branchId + ".txt"; BeanWrapperFieldExtractor<Student> fieldExtractor = new BeanWrapperFieldExtractor<>(); fieldExtractor.setNames(new String[]{"branchId", "name"}); DelimitedLineAggregator<Student> lineAggregator = new DelimitedLineAggregator<>(); lineAggregator.setFieldExtractor(fieldExtractor); FlatFileItemWriter<Student> writer = new FlatFileItemWriter<>(); writer.setResource(new FileSystemResource(fileName)); writer.setLineAggregator(lineAggregator); writer.setHeaderCallback(writerOut -> writerOut.write("Header")); writer.setFooterCallback(writerOut -> writerOut.write("Footer")); writers.put(branchId, writer); return writer; }; } } // 你的Student实体类(补充完整) class Student { private String name; private Integer branchId; public Student(String name, Integer branchId) { this.name = name; this.branchId = branchId; } public String getName() { return name; } public Integer getBranchId() { return branchId; } }
关键修改点解释
- 注入StepExecution:在
@StepScope的itemWriterClassifier方法中直接注入StepExecution,通过它获取ExecutionContext来存储writer,替代原来的实例变量,确保状态跨chunk保留。 - 移除手动
open()调用:Spring Batch会在ClassifierCompositeItemWriter.open()时自动调用所有子writer的open()方法;在step结束时,ClassifierCompositeItemWriter.close()会遍历所有子writer调用close(),这时候页脚回调就会被触发执行。 - 移除
appendAllowed=true:首次创建writer时,Spring Batch会自动初始化文件并写入页眉;后续复用writer时会自动追加数据,无需手动配置。
这样修改后,每个分支文件都会正确包含页眉、对应数据和页脚了。
内容的提问来源于stack exchange,提问作者RKP2019
相关产品推荐
相关产品推荐

