1. 从一次批处理任务卡住说起:MyBatisPagingItemReader 分页读取为什么读不到第二页
先说一个我实际遇到的场景。有个朋友做用户行为数据的离线归档,用 Spring Batch 配 MyBatis 做读写,任务跑起来之后日志显示读取了 200 条就停了,数据库里明明有几十万条。他一开始怀疑是 chunk 配置问题,改来改去没效果,最后发现是分页 SQL 里没写_skiprows和_pagesize这两个参数,reader 每次都在读同一页数据,Spring Batch 检测到返回结果和上一页重复,直接判定读取结束。
这个坑很典型。Mybatis-Spring 从 1.1.0 开始提供了三个专门给 Spring Batch 用的 bean:MyBatisPagingItemReader、MyBatisCursorItemReader、MyBatisBatchItemWriter。2.0.0 之后又补了对应的 builder 类,Java 配置写起来更顺手。但官方文档给的是片段,真正拼成一条能跑的读写链路,中间有不少细节要对齐。
这篇就围绕这三类组件,把分页读取、游标读取、批量写入的配置要点拆开讲,每个环节都给可复制的 XML 和 Java 配置,再配上验证动作。适合已经在用 Spring Batch、想把 MyBatis 接进 Reader/Writer 的同学,也适合刚开始搭批处理任务、被分页参数和批量提交绕晕的人。
核心检索词先摆出来:Mybatis-Spring 集成 Spring Batch 时,MyBatisPagingItemReader负责分页读,MyBatisCursorItemReader负责游标读,MyBatisBatchItemWriter负责批量写。三者配合 chunk 机制,才能把「读一批、处理一批、写一批」的流程跑通。
下面按「先讲清楚每个组件的行为 → 给配置 → 给验证」的顺序展开。如果你现在正卡在某个报错上,可以直接跳到第 5 节的排查对照表。
2. 接入前的准备:TaoToken 与 Mybatis-Spring 环境怎么摆
在动手写配置之前,先把两件事理清楚:一是 MyBatis 和 Mybatis-Spring 的依赖版本,二是如果你打算用大模型辅助生成 Mapper 或排查报错,怎么把模型调用接进来。
2.1 依赖版本的最低要求
MyBatisCursorItemReader依赖 MyBatis 3.4.0 及以上版本,因为游标读取走的是selectCursor()方法,低版本没有这个 API。MyBatisBatchItemWriter的复合写入技巧要求 MyBatis 3.2+,再早的版本有已知的 writer 行为问题。Mybatis-Spring 本身建议 2.0.0 以上,这样三个 builder 类都能用。
Maven 里大致是这样:
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.13</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.1.2</version> </dependency> <dependency> <groupId>org.springframework.batch</groupId> <artifactId>spring-batch-core</artifactId> <version>4.3.10</version> </dependency>版本不用完全照抄,但 MyBatis 别低于 3.4.0,Mybatis-Spring 别低于 2.0.0。
2.2 用 TaoToken 辅助生成配置和排查报错
写批处理配置的时候,经常需要查某个 builder 方法叫什么、某个参数怎么传。我习惯把这类问题丢给模型对话去问,比翻文档快。TaoToken 的模型对话入口可以直接用:
https://taotoken.net/api如果你要长期做编码类任务,比如反复生成 Mapper XML、调试 Spring Batch 的 chunk 配置,用 Coding Plan 会更划算,入口在:
https://taotoken.net/api/coding-planAPI Key 在控制台生成:
https://taotoken.net/api/console生成之后,在调用模型时把 Base URL 指向https://taotoken.net/api,Key 填进去,Model ID 按你选的模型填。这三件套(Base URL + Key + Model ID)是接入任何兼容 OpenAI 协议的工具时都要对齐的。
注意:TaoToken 是模型调用入口,不是数据库连接工具,也不替代你的 IDE。它帮你生成配置片段、解释报错,但最终跑批处理还是在你自己的 Spring Boot 工程里。
2.3 两个 SqlSessionFactory 的准备
批处理场景里,读和写经常要用不同的执行类型。MyBatisBatchItemWriter要求SqlSessionFactory配置成BATCH执行类型,而普通的读取用SIMPLE就行。所以工程里通常会有两个 factory:
@Configuration public class SessionFactoryConfig { @Bean public SqlSessionFactory batchReadingSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/read/*.xml")); return factoryBean.getObject(); } @Bean public SqlSessionFactory batchWritingSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/write/*.xml")); org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setDefaultExecutorType(ExecutorType.BATCH); factoryBean.setConfiguration(configuration); return factoryBean.getObject(); } }读的 factory 用默认执行类型,写的 factory 显式设成BATCH。这一步不做,后面 writer 写入时不会走批量,性能上不去,还可能报执行类型不匹配的错。
3. 三类组件的可复制配置:分页读、游标读、批量写
这一节是核心,把三个组件的 XML 和 Java 配置都给全,并且每个配置都说明关键参数怎么填。
3.1 MyBatisPagingItemReader 分页读取配置
分页读取的原理是:reader 执行你指定的queryId,SQL 里用_page、_pagesize、_skiprows三个参数构造分页。_page从 0 开始,_pagesize是每页行数,_skiprows是_page * _pagesize。
Mapper XML 里的查询这样写:
<select id="getEmployee" resultMap="employeeBatchResult"> SELECT id, name, job FROM employees ORDER BY id ASC LIMIT #{_skiprows}, #{_pagesize} </select>注意ORDER BY必须有,否则分页结果不稳定,可能出现重复或漏读。
XML 配置 reader:
<bean id="reader" class="org.mybatis.spring.batch.MyBatisPagingItemReader"> <property name="sqlSessionFactory" ref="batchReadingSessionFactory" /> <property name="queryId" value="com.my.name.space.batch.EmployeeMapper.getEmployee" /> <property name="pageSize" value="200" /> </bean>Java 配置用 builder:
@Bean public MyBatisPagingItemReader<Employee> reader() { return new MyBatisPagingItemReaderBuilder<Employee>() .sqlSessionFactory(batchReadingSessionFactory()) .queryId("com.my.name.space.batch.EmployeeMapper.getEmployee") .pageSize(200) .build(); }pageSize要和 step 的 chunk size 对齐。如果 chunk 是 200,pageSize 也设 200,这样每次读取刚好填满一个 chunk。
带参数的复杂场景,比如按时间范围读,用parameterValues传 map:
@StepScope @Bean public MyBatisPagingItemReader<User> dateBasedCriteriaReader( @Value("#{@datesParameters}") Map<String, Object> datesParameters) throws Exception { return new MyBatisPagingItemReaderBuilder<User>() .sqlSessionFactory(batchReadingSessionFactory()) .queryId("com.my.name.space.batch.ExampleMapper.queryUserInteractionsOnSpecificTimeSlot") .parameterValues(datesParameters) .pageSize(200) .build(); }对应的 XML 配置:
<bean id="dateBasedCriteriaReader" class="org.mybatis.spring.batch.MyBatisPagingItemReader" p:sqlSessionFactory-ref="batchReadingSessionFactory" p:parameterValues-ref="datesParameters" p:queryId="com.my.name.space.batch.ExampleMapper.queryUserInteractionsOnSpecificTimeSlot" p:pageSize="200" scope="step"/> <util:map id="datesParameters" scope="step"> <entry key="yesterday" value="#{jobExecutionContext['EXTRACTION_START_DATE']}"/> <entry key="today" value="#{jobExecutionContext['TODAY_DATE']}"/> </util:map>这里scope="step"很关键。reader 和参数 map 都必须是 step 作用域,才能在 SpEL 里访问jobExecutionContext。如果写成单例,启动时就会报找不到 jobExecutionContext 的错。
3.2 MyBatisCursorItemReader 游标读取配置
游标读取适合数据量大、不想一次性算分页的场景。它执行selectCursor(),每次read()返回游标的下一个元素,直到没有为止。用的是单独的数据库连接,不参与 step 里创建的事务。
Mapper 里写普通查询就行,不需要分页参数:
<select id="getEmployee" resultMap="employeeBatchResult"> SELECT id, name, job FROM employees ORDER BY id ASC </select>XML 配置:
<bean id="reader" class="org.mybatis.spring.batch.MyBatisCursorItemReader"> <property name="sqlSessionFactory" ref="batchReadingSessionFactory" /> <property name="queryId" value="com.my.name.space.batch.EmployeeMapper.getEmployee" /> </bean>Java 配置:
@Bean public MyBatisCursorItemReader<Employee> reader() { return new MyBatisCursorItemReaderBuilder<Employee>() .sqlSessionFactory(batchReadingSessionFactory()) .queryId("com.my.name.space.batch.EmployeeMapper.getEmployee") .build(); }游标读取不需要pageSize,因为它是流式返回。但要注意,游标持有的连接在 step 执行期间一直不释放,如果数据库连接池很小,同时跑多个游标任务可能把连接占满。
3.3 MyBatisBatchItemWriter 批量写入配置
writer 走SqlSessionTemplate的批量处理,SqlSessionFactory必须是BATCH执行类型。调用write()时执行statementId指定的语句,通常要放在事务里。
XML 配置:
<bean id="writer" class="org.mybatis.spring.batch.MyBatisBatchItemWriter"> <property name="sqlSessionFactory" ref="batchWritingSessionFactory" /> <property name="statementId" value="com.my.name.space.batch.EmployeeMapper.updateEmployee" /> </bean>Java 配置:
@Bean public MyBatisBatchItemWriter<User> writer() { return new MyBatisBatchItemWriterBuilder<User>() .sqlSessionFactory(batchWritingSessionFactory()) .statementId("com.my.name.space.batch.EmployeeMapper.updateEmployee") .build(); }默认情况下,writer 把 reader 读到的对象(或 processor 转换后的对象)直接作为参数传给 MyBatis。如果你想自定义参数对象,用itemToParameterConverter:
public class ItemToParameterMapConverters { public static <T> Converter<T, Map<String, Object>> createItemToParameterMapConverter( String operationBy, LocalDateTime operationAt) { return item -> { Map<String, Object> parameter = new HashMap<>(); parameter.put("item", item); parameter.put("operationBy", operationBy); parameter.put("operationAt", operationAt); return parameter; }; } }配置 writer 时挂上转换器:
@Bean public MyBatisBatchItemWriter<Person> writer() throws Exception { return new MyBatisBatchItemWriterBuilder<Person>() .sqlSessionFactory(batchWritingSessionFactory()) .statementId("org.mybatis.spring.sample.mapper.PersonMapper.createPerson") .itemToParameterConverter( createItemToParameterMapConverter("batch_java_config_user", LocalDateTime.now())) .build(); }对应的 insert 语句用#{item.firstName}这种形式取参数:
<insert id="createPerson"> insert into persons (first_name, last_name, operation_by, operation_at) values(#{item.firstName}, #{item.lastName}, #{operationBy}, #{operationAt}) </insert>3.4 复合 writer 写多表
如果要写多个有关联的表,用CompositeItemWriter把多个 writer 串起来,顺序很重要:
@Bean public CompositeItemWriter<?> interactionsItemWriter() { CompositeItemWriter compositeItemWriter = new CompositeItemWriter(); List<ItemWriter<?>> writers = new ArrayList<>(4); writers.add(visitorInteractionsWriter()); writers.add(customerInteractionsWriter()); writers.add(interactionMetadataWriter()); writers.add(interactionWriter()); compositeItemWriter.setDelegates(writers); return compositeItemWriter; }先写InteractionMetadata,拿到自增主键后,再写Interaction。Mapper 里用useGeneratedKeys和keyProperty把主键回填到对象上:
<insert id="insertInteractionMetadata" parameterType="com.my.batch.interactions.item.InteractionRecordToWriteInMultipleTables" useGeneratedKeys="true" keyProperty="interaction.interactionMetadata.id" keyColumn="id"> </insert>这里有个坑:不同 JDBC 驱动对批量模式下返回主键的行为不一致。H2 的 1.3.168 驱动只在 BATCH 模式下返回最后一个索引值,MySQL 驱动则正常返回所有 ID。如果发现关联表写入时主键对不上,先确认驱动版本和批量模式下的主键返回行为。
4. 验证请求与成功结果:怎么确认读写链路真的通了
配置写完不代表能跑。这一节给几个验证动作,从读取到写入逐段确认。
4.1 验证分页读取是否翻页
最直接的办法是在 Mapper 的 SQL 里临时加日志,或者在 reader 外面包一层监听。更简单的是看 Spring Batch 的日志:如果分页正常,你会看到 reader 反复执行查询,每次_skiprows递增。
也可以在测试里手动调 reader:
@Test public void testPagingReader() throws Exception { MyBatisPagingItemReader<Employee> reader = reader(); reader.open(new ExecutionContext()); Employee first = reader.read(); assertNotNull(first); Employee second = reader.read(); assertNotNull(second); assertNotEquals(first.getId(), second.getId()); reader.close(); }如果第二次读到的和第一次一样,说明分页参数没生效,回去检查 SQL 里有没有用_skiprows和_pagesize。
4.2 验证游标读取是否流式返回
游标读取的验证类似,但要注意它用的是独立连接:
@Test public void testCursorReader() throws Exception { MyBatisCursorItemReader<Employee> reader = reader(); reader.open(new ExecutionContext()); int count = 0; Employee item; while ((item = reader.read()) != null) { count++; } reader.close(); assertTrue(count > 0); }如果读到一半报连接超时,检查连接池配置和游标持有时间。
4.3 验证批量写入是否真的批量提交
批量写入的验证要看两点:一是数据有没有写进去,二是是不是批量提交的。可以在 writer 执行后查数据库:
@Test public void testBatchWriter() throws Exception { MyBatisBatchItemWriter<Employee> writer = writer(); List<Employee> items = Arrays.asList(emp1, emp2, emp3); writer.write(items); // 查库确认 Integer count = jdbcTemplate.queryForObject( "select count(*) from employees where id in (?,?,?)", Integer.class, emp1.getId(), emp2.getId(), emp3.getId()); assertEquals(Integer.valueOf(3), count); }批量提交的确认可以看 MyBatis 日志里的JDBC Connection和PreparedStatement执行次数。如果每条都单独提交,说明SqlSessionFactory没设成BATCH。
4.4 用模型对话核对配置
配置多的时候容易漏参数。我试过把 XML 和 Java 配置贴给模型对话,让它对照官方文档检查有没有缺项。入口还是:
https://taotoken.net/api比如问「MyBatisPagingItemReader 的 parameterValues 在 XML 里怎么配」,它会给出p:parameterValues-ref的写法。比自己翻文档快,但生成的内容还是要自己核对一遍。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth 对照
这一节把批处理集成里常见的报错和排查方向列出来,对照着看。
5.1 401 与鉴权类报错
如果你在用模型辅助生成配置时遇到 401,通常是 API Key 没填对或过期。检查控制台里的 Key 是否还有效:
https://taotoken.net/api/console批处理本身的 401 一般不会出现,因为数据库连接是本地配置的。但如果你的 Mapper 里调了外部服务,可能会有鉴权问题,那就和批处理无关了。
5.2 local proxy failed
这个报错通常出现在网络请求走本地代理时。如果你在调用模型接口时看到local proxy failed,检查你的 HTTP 客户端有没有配代理,以及代理是否可达。TaoToken 的接口地址是https://taotoken.net/api,不需要额外配代理。
5.3 reading choices 相关报错
如果你在解析模型返回时看到reading choices相关的错误,通常是返回结构和你预期的格式不一致。检查请求里的model参数和返回的choices数组。这类问题在模型对话里问一下就能定位:
https://taotoken.net/api5.4 OAuth 与 Claude Code 接入
如果你用 Claude Code 做编码辅助,接入时可能需要配置 OAuth 或 API Key。Claude Code 的 Anthropic 兼容入口在:
https://taotoken.net/api/claude-code-anthropic配置时同样要对齐三件套:Base URL、Key、Model ID。如果 OAuth 流程卡住,先确认回调地址和 Key 的权限范围。
5.5 批处理本身的常见错
除了上面这些,批处理集成里还有几个高频错:
| 报错现象 | 可能原因 | 排查方向 |
|---|---|---|
| 读取只返回第一页 | SQL 没用_skiprows/_pagesize | 检查 Mapper XML |
| 游标读取报连接超时 | 连接池太小或游标持有太久 | 调大连接池或改用分页 |
| 批量写入不生效 | SqlSessionFactory不是 BATCH | 检查defaultExecutorType |
| 关联表主键对不上 | 驱动批量模式主键返回行为不同 | 换驱动版本或改写入顺序 |
| step 作用域报错 | reader 或参数 map 写成单例 | 加@StepScope或scope="step" |
排查的时候,先把 Spring Batch 的日志级别调到 DEBUG,看 reader 和 writer 的实际执行次数。再对照上面的表逐项排除。
6. 把读写链路跑通之后:几个实用建议
配置跑通只是第一步。实际用的时候,有几个点值得注意。
分页读取的pageSize和 chunk size 对齐之后,内存占用和数据库压力都比较可控。如果数据量特别大,分页比游标更稳,因为游标长时间持有连接,容易在连接池紧张时出问题。
批量写入的SqlSessionFactory一定要单独配一个 BATCH 类型的,不要和读取共用。共用的话,要么读取变慢,要么写入不批量,两头不讨好。
复合 writer 写多表的时候,顺序不能乱。先写主表拿到主键,再写关联表。如果驱动在批量模式下主键返回有问题,可以考虑把关联表的写入拆成单独的 step,或者改用非批量模式写关联表。
最后,如果你在生成 Mapper 或调试配置时需要快速查 API,模型对话和 Coding Plan 都能用。API Key 在控制台生成,Base URL 指向https://taotoken.net/api,Model ID 按需选。三件套对齐了,接入就顺了。