ItemReader 详解
本章定位:深入理解 ItemReader——Spring Batch 数据入口的核心抽象,掌握 FlatFileItemReader、JdbcCursorItemReader、JdbcPagingItemReader 等内置实现的使用和选型。
定义与作用
ItemReader<T> 是 Spring Batch 中的核心读接口,定义了逐条读取数据的抽象。它只有一个方法:
@FunctionalInterface
public interface ItemReader<T> {
@Nullable
T read() throws Exception;
}
飞翔科技后端开发小崔的理解:
"ItemReader 就像一个快递拆包员——每次调用 read() 就拆出一件包裹。返回 null 表示包裹拆完了。Spring Batch 提供了 FlatFileItemReader(拆文件)、JdbcCursorItemReader(从数据库流式取)和 JdbcPagingItemReader(分页取)等现成的拆包员。"
核心原理
内置 Reader 选型决策
游标 vs 分页
| 维度 | JdbcCursorItemReader | JdbcPagingItemReader |
|---|---|---|
| 读取方式 | 打开游标,逐行流式读取 | 每页 SELECT + LIMIT/OFFSET |
| 数据库连接 | 全程占用一个连接 | 每页获取/释放连接 |
| 内存占用 | 低(逐行) | 低(每页加载) |
| 适用场景 | 中小数据量、简单查询 | 大数据量、长时间运行 |
| 线程安全 | 否(游标状态) | 分页参数可隔离 |
| 状态保存 | 行号 | 页码 + 页内偏移 |
完整示例
场景一:飞翔科技——CSV 文件读取学生成绩
背景:运营部高英提供的学生成绩 CSV 文件 grades.csv:
student_id,course_name,score
STU001,数据结构,85
STU002,操作系统,92
STU003,计算机网络,78
@Bean
@StepScope
public FlatFileItemReader<GradeDTO> gradeCsvReader(
@Value("#{jobParameters['input.file']}") String inputFile) {
return new FlatFileItemReaderBuilder<GradeDTO>()
.name("gradeCsvReader")
.resource(new FileSystemResource(inputFile))
.delimited()
.delimiter(",")
.names("studentId", "courseName", "score")
.targetType(GradeDTO.class)
.linesToSkip(1) // 跳过 CSV 标题行
.strict(true) // 文件不存在时抛异常
.build();
}
场景二:数据库分页读取
@Bean
public JdbcPagingItemReader<Order> orderDbReader(DataSource dataSource) {
return new JdbcPagingItemReaderBuilder<Order>()
.name("orderDbReader")
.dataSource(dataSource)
.selectClause("SELECT order_id, customer_name, amount, order_date")
.fromClause("FROM orders")
.whereClause("WHERE status = 'PENDING' AND order_date = ?")
.parameterValues(Map.of("orderDate", "2026-06-13"))
.sortKeys(Map.of("order_id", Order.ASCENDING)) // 分页必须排序!
.rowMapper(new BeanPropertyRowMapper<>(Order.class))
.pageSize(100)
.saveState(true)
.build();
}
操作前后对比:
操作前(手动 JDBC):
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM orders WHERE status='PENDING'");
while (rs.next()) { ... } // 手动循环、无分页、无断点续传
操作后(JdbcPagingItemReader):
框架自动分页、每页 100 条、saveState 自动记录分页位置
崩溃后从上次的页偏移自动恢复
易错场景与避坑
反例一:分页 Reader 忘记设置 sortKeys
// ❌ 没有 sortKeys → 分页结果顺序不确定!
return new JdbcPagingItemReaderBuilder<Order>()
.name("reader")
.dataSource(dataSource)
.selectClause("SELECT * FROM orders")
.fromClause("FROM orders")
.pageSize(100)
// 缺少 .sortKeys() → 分页时数据可能重复或遗漏!
.build();
反例二:cursor reader 在 partition 中使用
// ❌ JdbcCursorItemReader 非线程安全 → 不能用于分区 Step
@Bean
@StepScope
public JdbcCursorItemReader<Order> reader(DataSource ds) {
// ... 分区时多个线程共享游标 → 数据错乱
}
正确做法:分区场景使用 PagingItemReader 或设置 saveState=false 并自行处理并发。
面试高频考点
Q1:JdbcCursorItemReader 和 JdbcPagingItemReader 的区别?
Cursor:保持游标打开,逐行流式读取,占用一个连接全程不释放,非线程安全。Paging:分页查询,每页释放连接,通过 sortKeys 保证分页一致性,线程安全。大数据量和分区场景优先 Paging。
Q2:ItemReader.read() 返回 null 代表什么?
返回 null 表示数据已全部读完,Step 结束当前 Chunk 并进入完成流程。这是 Reader 和 Processor 返回 null 的本质区别——Reader 的 null 终止 Step,Processor 的 null 仅过滤当前条。