FlatFileItemWriter 详解
本章定位:深入掌握 FlatFileItemWriter——将批处理结果导出为 CSV/定长文件,包括格式化、头和尾回调、文件管理和资源清理。
定义与作用
FlatFileItemWriter 将 Chunk 中的数据逐行写入平面文件。支持定界符(CSV/TSV)和定长两种格式,通过 LineAggregator 将领域对象转换为文本行。
小崔在导出杨英需要的报表时发现:
"FlatFileItemWriter 不只是写文件——
headerCallback写 CSV 标题行,footerCallback写统计信息,LineAggregator控制每行的格式。导出 10 万条数据就像写 1 条一样简单。"
核心原理
完整示例
场景一:CSV 成绩报表导出
@Bean
@StepScope
public FlatFileItemWriter<GradeReport> gradeReportWriter(
@Value("#{jobParameters['output.file']}") String outputFile) {
// 自定义 LineAggregator:格式化每行
DelimitedLineAggregator<GradeReport> aggregator = new DelimitedLineAggregator<>();
aggregator.setDelimiter(",");
aggregator.setFieldExtractor(report -> new String[]{
report.getStudentId(),
report.getStudentName(),
report.getCourse(),
String.format("%.1f", report.getScore()),
report.getGrade()
});
return new FlatFileItemWriterBuilder<GradeReport>()
.name("gradeReportWriter")
.resource(new FileSystemResource(outputFile))
.encoding("UTF-8")
.lineAggregator(aggregator)
.headerCallback(writer -> writer.write("学号,姓名,课程,成绩,等级"))
.footerCallback(writer -> {
writer.write("# 报表生成时间: " + LocalDateTime.now());
})
.shouldDeleteIfExists(true) // 先删除旧文件
.transactional(true) // 参与 Step 事务
.build();
}
输出文件(grades_report.csv):
学号,姓名,课程,成绩,等级
STU001,张三,数据结构,85.0,B
STU002,李四,操作系统,92.0,A
STU003,王五,计算机网络,78.0,C
# 报表生成时间: 2026-06-13T14:30:00
场景二:定长银行对账文件
@Bean
public FlatFileItemWriter<BankRecord> fixedLengthWriter() {
FormatterLineAggregator<BankRecord> aggregator = new FormatterLineAggregator<>();
aggregator.setFormat("%-8s%-6s%-20s%-30s%03d"); // 定长格式
aggregator.setFieldExtractor(record -> new Object[]{
record.getDate(),
record.getStudentId(),
record.getName(),
record.getCourse(),
record.getScore()
});
return new FlatFileItemWriterBuilder<BankRecord>()
.name("bankRecordWriter")
.resource(new FileSystemResource("bank_output.txt"))
.lineAggregator(aggregator)
.build();
}
易错场景与避坑
反例一:重启时不删除旧文件导致追加
// ❌ shouldDeleteIfExists=false 且 restart 时 → 数据追加
// 第一次执行写了 1000 行,重启后又写 1000 行 → 文件有 2000 行
return new FlatFileItemWriterBuilder<>()
.resource(new FileSystemResource("output.csv"))
.shouldDeleteIfExists(false) // ❌ 重启时文件还在
.build();
正确做法:设置 shouldDeleteIfExists(true) 或使用 shouldDeleteIfEmpty(true)。
反例二:忘记 transactional 导致数据不一致
// ❌ transactional=false → Writer 操作不参与 Chunk 事务
return new FlatFileItemWriterBuilder<>()
.transactional(false) // ❌ 数据库写入成功但文件写入失败时无法回滚
.build();
面试高频考点
Q1:headerCallback 和 footerCallback 的调用时机?
headerCallback在FlatFileItemWriter.open()中调用(文件刚打开时)。footerCallback在FlatFileItemWriter.close()中调用(文件关闭前,所有数据行写入后)。两者都只调用一次。
Q2:如何实现按条件动态切换输出文件?
使用
ClassifierCompositeItemWriter,根据数据特征(如 type 字段)将记录路由到不同的 FlatFileItemWriter。每个 Writer 配置不同的 resource,实现分类写入。