Spring Batch 6.1.0-RC1 新增了 PageableItemReader。旧的 RepositoryItemReader 靠 methodName 字符串反射仓库方法;新 Reader 直接吃 Function<Pageable, Slice<T>>,编译期就能对上方法引用。用 Spring Boot 4.2.0-RC1(BOM 绑 Batch 6.1.0-RC1)加 Spring Data JPA 与 H2,同一套 findByCity 分页查询两边各跑一遍:pageSize=3、按 id 升序时都读出 11 条 Shanghai,chunk 切成 3/3/3/2;把旧写法的方法名写错,则要等到 Job 运行才炸出 NoSuchMethodException。

Javadoc 标了 @since 6.1,GitHub PR #5477 与 6.1.0-RC1 Release Notes 都把它列为新特性。昨天那篇 @JobParameter 讲的是作业参数注入;更早的 6.1.0-M2 谈异步 API 迁 core。本文只补这条类型安全的分页读,不重复那些主题。

环境和依赖

实测环境:JDK 21.0.12.1、Spring Boot 4.2.0-RC1、Spring Batch 6.1.0-RC1、Spring Data JPA 4.2.0-RC1、H2 2.5.252、Hibernate ORM 7.4.13.Final。Boot 4 的 Batch JDBC 自动配置在 spring-boot-starter-batch-jdbc:

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>4.2.0-RC1</version>
</parent>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-batch-jdbc</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <scope>runtime</scope>
</dependency>

dependency:tree 对齐关系:

+- org.springframework.boot:spring-boot-starter-batch-jdbc:jar:4.2.0-RC1:compile
|  +- org.springframework.boot:spring-boot-starter-batch:jar:4.2.0-RC1:compile
|  |     \- org.springframework.batch:spring-batch-core:jar:6.1.0-RC1:compile
|  |        +- org.springframework.batch:spring-batch-infrastructure:jar:6.1.0-RC1:compile
+- org.springframework.boot:spring-boot-starter-data-jpa:jar:4.2.0-RC1:compile
|  +- org.springframework.data:spring-data-jpa:jar:4.2.0-RC1:compile
\- com.h2database:h2:jar:2.5.252:runtime

application.properties 开 JPA 建表、Batch schema 初始化,关掉 Web:

spring.datasource.url=jdbc:h2:mem:batchpageable;DB_CLOSE_DELAY=-1
spring.jpa.hibernate.ddl-auto=create-drop
spring.batch.jdbc.initialize-schema=always
spring.main.web-application-type=none

启动前先种 12 条客户:11 条 city=Shanghai,1 条 Beijing。后面两条 Job 都只读 Shanghai。

同一仓库方法,两种 Reader

仓库方法返回 Slice,最后一个参数是 Pageable:

public interface CustomerRepository extends JpaRepository<Customer, Long> {
    Slice<Customer> findByCity(String city, Pageable pageable);
}

旧写法还是把方法名写成字符串,附加参数列表靠反射拼到最后的 Pageable:

RepositoryItemReader<Customer> reader = new RepositoryItemReaderBuilder<Customer>()
        .name("repositoryCustomerReader")
        .repository(customerRepository)
        .methodName("findByCity")
        .arguments(List.of("Shanghai"))
        .sort(Sort.by(Sort.Direction.ASC, "id"))
        .pageSize(3)
        .build();

新写法把查询收成函数,方法引用在编译期校验:

PageableItemReader<Customer> reader = new PageableItemReaderBuilder<Customer>()
        .name("pageableCustomerReader")
        .query(pageable -> customerRepository.findByCity("Shanghai", pageable))
        .sort(Sort.by(Sort.Direction.ASC, "id"))
        .pageSize(3)
        .build();

官方示例常用 customerRepository::findAll;这里要固定城市,就用 lambda 把 "Shanghai" 封进去。Builder 的 query、sort、pageSize、name 与 Javadoc 一致。Step 用 Batch 6.1 的 chunk(size).transactionManager(...) 新写法,chunk size 也设成 3,方便和分页对齐。

两边各跑一遍

用 demo.reader 切换 Reader,作业参数 run.id 区分实例:

mvn spring-boot:run -Dspring-boot.run.arguments="--demo.reader=pageable run.id=pageable-1"
mvn spring-boot:run -Dspring-boot.run.arguments="--demo.reader=repository run.id=repository-1"

Pageable 路径关键日志(时间戳原样,已去掉启动行里的本地路径):

 :: Spring Boot ::            (v4.2.0-RC1)
2026-11-04T08:05:45.031+08:00  INFO ... CustomerJobConfig : using readerMode=pageable, readerClass=PageableItemReader
2026-11-04T08:05:45.524+08:00  INFO ... CustomerDataInitializer : seeded customers total=12, shanghai=11
2026-11-04T08:05:45.634+08:00  INFO ... TaskExecutorJobLauncher : Job: [SimpleJob: [name=customerJob]] launched with the following parameters: [{JobParameter{name='run.id', value=pageable-1, type=class java.lang.String, identifying=true}}]
2026-11-04T08:05:45.669+08:00  INFO ... CustomerJobConfig : [pageable] read #1 -> Customer{id=1, name='Ada', city='Shanghai'}
2026-11-04T08:05:45.670+08:00  INFO ... CustomerJobConfig : [pageable] chunk wrote ids=[1, 2, 3], chunkSize=3, cumulative=3
2026-11-04T08:05:45.677+08:00  INFO ... CustomerJobConfig : [pageable] chunk wrote ids=[4, 5, 6], chunkSize=3, cumulative=6
2026-11-04T08:05:45.679+08:00  INFO ... CustomerJobConfig : [pageable] chunk wrote ids=[7, 8, 9], chunkSize=3, cumulative=9
2026-11-04T08:05:45.682+08:00  INFO ... CustomerJobConfig : [pageable] read #11 -> Customer{id=11, name='Kim', city='Shanghai'}
2026-11-04T08:05:45.682+08:00  INFO ... CustomerJobConfig : [pageable] chunk wrote ids=[10, 11], chunkSize=2, cumulative=11
2026-11-04T08:05:45.689+08:00  INFO ... TaskExecutorJobLauncher : Job: [SimpleJob: [name=customerJob]] completed ... status: [COMPLETED] in 49ms

Repository 路径同一套数据:

2026-11-04T08:05:54.011+08:00  INFO ... CustomerJobConfig : using readerMode=repository, readerClass=RepositoryItemReader
2026-11-04T08:05:54.458+08:00  INFO ... CustomerDataInitializer : seeded customers total=12, shanghai=11
2026-11-04T08:05:54.552+08:00  INFO ... CustomerJobConfig : [repository] read #1 -> Customer{id=1, name='Ada', city='Shanghai'}
2026-11-04T08:05:54.554+08:00  INFO ... CustomerJobConfig : [repository] chunk wrote ids=[1, 2, 3], chunkSize=3, cumulative=3
2026-11-04T08:05:54.560+08:00  INFO ... CustomerJobConfig : [repository] chunk wrote ids=[4, 5, 6], chunkSize=3, cumulative=6
2026-11-04T08:05:54.563+08:00  INFO ... CustomerJobConfig : [repository] chunk wrote ids=[7, 8, 9], chunkSize=3, cumulative=9
2026-11-04T08:05:54.566+08:00  INFO ... CustomerJobConfig : [repository] read #11 -> Customer{id=11, name='Kim', city='Shanghai'}
2026-11-04T08:05:54.566+08:00  INFO ... CustomerJobConfig : [repository] chunk wrote ids=[10, 11], chunkSize=2, cumulative=11
2026-11-04T08:05:54.574+08:00  INFO ... TaskExecutorJobLauncher : Job: [SimpleJob: [name=customerJob]] completed ... status: [COMPLETED] in 55ms

对照很干净:两边都是 Ada → Kim、累计 11、最后一页 2 条、状态 COMPLETED。Beijing 那条没进来。分页语义和排序对齐,换 Reader 不改查询结果。

方法名写错会怎样

把旧 Reader 的 methodName 改成 findByCityTypo,编译照样过,Job 跑到读第一页才失败:

org.springframework.batch.infrastructure.item.adapter.DynamicMethodInvocationException: java.lang.NoSuchMethodException: jdk.proxy2.$Proxy103.findByCityTypo(java.lang.String,org.springframework.data.domain.PageRequest)
...
Job: [SimpleJob: [name=customerJob]] completed ... status: [FAILED] in 47ms

这就是反射路径的代价:字符串错了,要等到 chunk 真正调仓库才暴露。PageableItemReader 用方法引用或 lambda 时,方法不存在或签名对不上,javac 当场拦住,不会拖到运行时。

分页与重启行为上,官方把新 Reader 设计得跟 RepositoryItemReader 接近;Builder 默认 saveState=true。这次没单独做失败重启回放,只确认了正常读完与错名失败两条路径。

迁代码时怎么对齐

有 Spring Data 仓库、末参是 Pageable、返回 Slice/Page 的读步骤,可以直接换成 PageableItemReaderBuilder:把 methodName + arguments 收成 query(...),sort/pageSize/name 保留。无参的 findAll(Pageable) 还能写成 repository::findAll。官方博文还提到可用 Sort.by(Sort.Direction.ASC, Customer::getId) 这类属性引用去掉排序字段字符串;本例为了两边对照,排序字段仍用 "id"。

RepositoryItemReader 还在,RC1 并未标记废弃。新类是类型安全的替代方案,不是强制迁移开关。和昨天的 @JobParameter 一样,都是 6.1 把「字符串晚绑定」往「编译期能看见」推一截。

参考:Spring Batch 6.1.0-RC1 API:PageableItemReader、PageableItemReaderBuilder、v6.1.0-RC1 Release Notes、PR #5477。

— 感谢阅读 —
广告

一起交流

分享你的思考,让讨论更进一步。