ItemWriter 详解
本章定位:深入理解 ItemWriter——Spring Batch 数据出口的核心抽象,掌握批量写入机制、事务保证和内置实现选型。
定义与作用
ItemWriter<T> 是 Spring Batch 中的核心写接口,负责将 Chunk 中的一组数据批量写出到目标系统(数据库、文件、消息队列)。
@FunctionalInterface
public interface ItemWriter<T> {
void write(Chunk<? extends T> chunk) throws Exception;
}
关键设计:write() 接收的是 Chunk(一组数据),不是单条——这保证了一次事务提交对应一次数据批量写出。
飞翔科技架构师白歌的比喻:
"Writer 是仓库管理员。Reader 一件一件搬进来,Processor 加工好堆在托盘上,等托盘堆满(Chunk Size 达标),Writer 一次性把一整个托盘入库。入库成功 → 事务提交。"
核心原理
Writer 在 Chunk 循环中的位置
内置 Writer 选型
| Writer | 数据目标 | 关键特性 |
|---|---|---|
JdbcBatchItemWriter | 数据库 | 使用 PreparedStatement.addBatch() 批量执行 |
FlatFileItemWriter | 文件 | 逐行写入,支持定长/分隔符格式 |
CompositeItemWriter | 多目标 | 同时写入多个 Writer |
StaxEventItemWriter | XML 文件 | 基于 StAX |
JsonFileItemWriter | JSON 文件 | 流式 JSON 生成 |
JpaItemWriter | JPA 数据库 | entityManager.merge() 逐条 |
KafkaItemWriter | Kafka | 批量发送消息 |
完整示例
场景一:飞翔科技——JdbcBatchItemWriter 批量写入学生数据
@Bean
public JdbcBatchItemWriter<StudentEntity> studentWriter(DataSource dataSource) {
return new JdbcBatchItemWriterBuilder<StudentEntity>()
.dataSource(dataSource)
.sql("INSERT INTO students (student_id, name, email, age, import_time) "
+ "VALUES (:studentId, :name, :email, :age, :importTime)")
.itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())
.assertUpdates(true) // 断言每次更新的行数
.build();
}
运行结果(Chunk Size=50):
Chunk 1: 50 items → PreparedStatement.addBatch() × 50 → executeBatch() → 50 rows inserted → COMMIT
Chunk 2: 50 items → ... → 50 rows → COMMIT
...
Chunk 100: 50 items → ... → 50 rows → COMMIT
Step 完成: writeCount=5000
场景二:FlatFileItemWriter 生成导出文件
@Bean
public FlatFileItemWriter<StudentEntity> studentExportWriter() {
return new FlatFileItemWriterBuilder<StudentEntity>()
.name("studentExportWriter")
.resource(new FileSystemResource("export_students.csv"))
.delimited()
.delimiter(",")
.names("studentId", "name", "email", "age")
.headerCallback(writer -> writer.write("student_id,name,email,age"))
.footerCallback(writer -> writer.write("# Generated by Spring Batch"))
.encoding("UTF-8")
.build();
}
操作前后对比:
操作前(手动 JDBC):
for (Student s : list) {
PreparedStatement ps = conn.prepareStatement("INSERT ...");
ps.setString(1, s.getId());
// ... 逐个设置、执行
}
conn.commit();
问题:每条一条 SQL,100 万条 = 100 万次网络往返
操作后(JdbcBatchItemWriter):
Chunk size 500 → 每 500 条 addBatch() → executeBatch() → 1 次网络往返
100 万条 = 2000 次网络往返(减少 500 倍)
易错场景与避坑
反例一:JdbcBatchItemWriter 使用错误的 Provider
// ❌ 使用 MapItemSqlParameterSourceProvider 但 SQL 用 :propertyName
return new JdbcBatchItemWriterBuilder<StudentEntity>()
.dataSource(dataSource)
.sql("INSERT INTO students VALUES (:studentId, :name, :email)")
.itemSqlParameterSourceProvider(
new MapItemSqlParameterSourceProvider()) // ❌ 需要 Map 但实体是 POJO
.build();
正确做法:POJO 实体用 BeanPropertyItemSqlParameterSourceProvider,Map 用 MapItemSqlParameterSourceProvider。
反例二:Writer 的 write() 中逐条提交
// ❌ 在 write() 内部逐条 commit → 破坏事务一致性
public void write(Chunk<? extends Student> chunk) {
for (Student s : chunk.getItems()) {
jdbcTemplate.update("INSERT ...", s.getId(), s.getName());
// ❌ 不要在这里手动提交!
}
}
面试高频考点
Q1:ItemWriter.write() 为什么接收 Chunk 而不是单条?
为了批量写入优化。JDBC 的
executeBatch()、文件的BufferedWriter.flush()都是批量操作。如果逐条写入,性能会极度下降。Chunk 机制保证了一次事务内尽可能多的数据被一次性写入。
Q2:JpaItemWriter 和 JdbcBatchItemWriter 的性能差异?
JpaItemWriter 逐条调用
entityManager.merge(),适合小数据量和需要 JPA 级联操作的场景。JdbcBatchItemWriter 使用 JDBC Batch API 批量写入,性能高 5-10 倍,适合大规模 ETL。