Spring Boot 4.1 新增了 spring-boot-starter-batch-data-mongodb,加上它、连上副本集模式的 MongoDB,再配一行 spring.batch.data.mongodb.schema.initialize=true,Spring Batch 的 JobRepository 就会自动落到 MongoDB 里。我用一个读 CSV 写 Mongo 的作业实测,第 57 条记录处失败时已写入 50 条,重启后只读剩下的 50 条,最终 100 条、没有重复。单机模式的 MongoDB 不行,作业一启动就报事务错误。

测试环境是 Spring Boot 4.1.1、Spring Batch 6.0.5、Spring Data MongoDB 5.1.1、MongoDB Java Driver 5.8.1、MongoDB 8.3.13、JDK 21.0.12.1。

这个特性 Josh Long 在 spring.io 上写过一篇介绍:MongoDB-backed Spring Batch jobs and more in Spring Boot 4.1,他的例子是 Mongo 存元数据、Postgres 存业务数据。我这里业务数据也写进 Mongo,重点放在自动配置的条件、断点续跑和元数据长什么样。

依赖和配置

pom 里只需要一个 starter:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-batch-data-mongodb</artifactId>
</dependency>

它会带进 Batch 和 Spring Data MongoDB:

[INFO] \- org.springframework.boot:spring-boot-starter-batch-data-mongodb:jar:4.1.1:compile
[INFO]    +- org.springframework.boot:spring-boot-starter-batch:jar:4.1.1:compile
[INFO]    |  \- org.springframework.boot:spring-boot-batch:jar:4.1.1:compile
[INFO]    |     \- org.springframework.batch:spring-batch-core:jar:6.0.5:compile
[INFO]    +- org.springframework.boot:spring-boot-starter-data-mongodb:jar:4.1.1:compile
[INFO]    |  |  \- org.mongodb:mongodb-driver-sync:jar:5.8.1:compile
[INFO]    |  \- org.springframework.boot:spring-boot-data-mongodb:jar:4.1.1:compile
[INFO]    |     \- org.springframework.data:spring-data-mongodb:jar:5.1.1:compile
[INFO]    \- org.springframework.boot:spring-boot-batch-data-mongodb:jar:4.1.1:compile

application.properties:

spring.mongodb.uri=mongodb://localhost:27017/batchdemo?replicaSet=rs0
spring.batch.data.mongodb.schema.initialize=true

Boot 4 里 Mongo 连接的前缀是 spring.mongodb,不是老的 spring.data.mongodb。MongoDB 我用 Docker 起了一个单节点副本集:

docker run -d --name batch-mongo -p 27017:27017 mongo:8 --replSet rs0 --bind_ip_all
docker exec batch-mongo mongosh --quiet --eval 'rs.initiate({_id:"rs0",members:[{_id:0,host:"localhost:27017"}]}).ok'

自动配置靠什么生效

起作用的是 spring-boot-batch-data-mongodb 模块里的 BatchDataMongoAutoConfiguration。带 --debug 启动,条件报告里写得很清楚:

   BatchDataMongoAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'org.springframework.batch.core.launch.JobOperator', 'com.mongodb.client.MongoClient', 'org.springframework.data.mongodb.core.MongoOperations' (OnClassCondition)
      - @ConditionalOnBean (types: org.springframework.data.mongodb.MongoDatabaseFactory; SearchStrategy: all) found bean 'mongoDatabaseFactory'; @ConditionalOnMissingBean (types: org.springframework.batch.core.configuration.support.DefaultBatchConfiguration; SearchStrategy: all) did not find any beans (OnBeanCondition)

   BatchDataMongoAutoConfiguration.BatchMongoDatabaseInitializerConfiguration matched:
      - @ConditionalOnProperty (spring.batch.data.mongodb.schema.initialize) matched (OnPropertyCondition)
      - @ConditionalOnBean (types: org.springframework.data.mongodb.core.MongoOperations; SearchStrategy: all) found bean 'mongoTemplate' (OnBeanCondition)

也就是说,classpath 上有 Batch、Mongo 驱动和 Spring Data MongoDB,容器里有 MongoDatabaseFactory,并且你自己没有声明 DefaultBatchConfiguration 或 @EnableBatchProcessing,它就会注册一个继承自 Batch MongoDefaultBatchConfiguration 的配置类。事务管理器优先用你标了 @BatchTransactionManager 的 MongoTransactionManager,没有就用容器里现成的,再没有就自己 new 一个。

建集合是另一个条件,只看 spring.batch.data.mongodb.schema.initialize,默认 false。打开后 Boot 会逐行执行 spring-batch-core 里的 schema-mongodb.jsonl:

{create:'BATCH_JOB_INSTANCE'}
{create:'BATCH_JOB_EXECUTION'}
{create:'BATCH_STEP_EXECUTION'}
{create:'BATCH_SEQUENCES'}
{insert: "BATCH_SEQUENCES", documents: [ { _id: 'BATCH_JOB_INSTANCE_SEQ', count: NumberLong(0) } ]}
{insert: "BATCH_SEQUENCES", documents: [ { _id: 'BATCH_JOB_EXECUTION_SEQ', count: NumberLong(0) } ]}
{insert: "BATCH_SEQUENCES", documents: [ { _id: 'BATCH_STEP_EXECUTION_SEQ', count: NumberLong(0) } ]}

spring.batch.data.mongodb 下面一共就这几个属性:schema.initialize、schema.location、validate-transaction-state(默认 true)、isolation-level-for-create。这些和条件我是对着 Boot v4.1.1 的 BatchDataMongoAutoConfiguration、BatchDataMongoProperties 源码和 Boot 参考文档的 Spring Batch 章节确认的。

作业本身

读 100 行 CSV,处理器把名字转大写,MongoItemWriter 写进 customers 集合,chunk 大小 10。demo.fail-at 用来人为制造失败:

@Bean
ItemProcessor<Customer, Customer> customerProcessor(@Value("${demo.fail-at:-1}") int failAt) {
    return c -> {
        if (c.id() == failAt) {
            throw new IllegalStateException("boom at customer " + c.id());
        }
        return new Customer(c.id(), c.name().toUpperCase(), c.email());
    };
}

@Bean
Step importStep(JobRepository repository, FlatFileItemReader<Customer> reader,
        ItemProcessor<Customer, Customer> processor, MongoItemWriter<Customer> writer) {
    return new StepBuilder("import-customers", repository)
            .<Customer, Customer>chunk(10)
            .reader(reader)
            .processor(processor)
            .writer(writer)
            .build();
}

@Bean
Job importJob(JobRepository repository, Step importStep) {
    return new JobBuilder("import-customers-job", repository).start(importStep).build();
}

Batch 6 把 infrastructure 的包挪到了 org.springframework.batch.infrastructure.item... 下,FlatFileItemReader、MongoItemWriter 都要从那里 import。作业参数直接跟在命令行后面,input=customers.csv 会成为一个 identifying 参数。

第 57 条失败,然后重启

第一次运行带上 --demo.fail-at=57:

java.lang.IllegalStateException: boom at customer 57
2026-10-15T08:11:29.374+08:00  INFO 889168 --- [batch-mongo-jobrepo-demo] [           main] o.s.b.c.l.s.TaskExecutorJobLauncher      : Job: [SimpleJob: [name=import-customers-job]] completed with the following parameters: [{JobParameter{name='input', value=customers.csv, type=class java.lang.String, identifying=true}}] and the following status: [FAILED] in 264ms
exit code: 5

这时去 Mongo 里看:

$ mongosh batchdemo --eval 'db.customers.countDocuments()'
50
    status: 'FAILED',
    readCount: Long('60'),
    writeCount: Long('50'),
    commitCount: Long('5'),
    rollbackCount: Long('1'),
    executionContext: {
      map: {
        'batch.version': '6.0.5',
        'batch.stepType': 'org.springframework.batch.core.step.item.ChunkOrientedStep',
        'customer-reader.read.count': 50

前 5 个 chunk 已经提交,第 6 个 chunk(51 到 60)整体回滚,所以读了 60 条、写进去 50 条。执行上下文里 customer-reader.read.count 记的是 50,这就是重启时的断点。

去掉 --demo.fail-at,参数不变再跑一次:

2026-10-15T08:11:38.238+08:00  INFO 890026 --- [batch-mongo-jobrepo-demo] [           main] o.s.b.c.l.s.TaskExecutorJobLauncher      : Job: [SimpleJob: [name=import-customers-job]] completed with the following parameters: [{JobParameter{name='input', value=customers.csv, type=class java.lang.String, identifying=true}}] and the following status: [COMPLETED] in 194ms
exit code: 0

第二次的 StepExecution:

    stepExecutionId: Long('2'),
    jobExecutionId: Long('2'),
    status: 'COMPLETED',
    readCount: Long('50'),
    writeCount: Long('50'),
    commitCount: Long('5'),
    rollbackCount: Long('0'),

只读了 50 条,5 次提交,集合里正好 100 条,按 _id 分组也没有重复:

$ mongosh batchdemo --eval 'db.customers.countDocuments()'
100
$ mongosh batchdemo --eval 'db.customers.aggregate([{$group:{_id:"$_id", n:{$sum:1}}},{$match:{n:{$gt:1}}}]).toArray().length'
0

两次执行挂在同一个 JobInstance 下面,jobInstanceId 都是 1,jobExecutionId 分别是 1 和 2。同样的参数第三次再跑,就会被拒绝:

Caused by: org.springframework.batch.core.launch.JobInstanceAlreadyCompleteException: A job instance already exists and is complete for identifying parameters={JobParameter{name='input', value=customers.csv, type=class java.lang.String, identifying=true}}.  If you want to run this job again, change the parameters.

元数据在 Mongo 里长什么样

集合还是熟悉的名字,只是变成了文档:

$ mongosh batchdemo --eval 'db.getCollectionNames().sort()'
[
  'BATCH_JOB_EXECUTION',
  'BATCH_JOB_INSTANCE',
  'BATCH_SEQUENCES',
  'BATCH_STEP_EXECUTION',
  'customers'
]

没有 BATCH_JOB_EXECUTION_PARAMS 和两张 context 表。参数、执行上下文都嵌在执行文档里,JobInstance 文档很短:

    jobInstanceId: Long('1'),
    jobName: 'import-customers-job',
    jobKey: 'ed4e2f375925d4ded5dd307749f5e059',
    _class: 'org.springframework.batch.core.repository.persistence.JobInstance'

ID 不靠 ObjectId,而是 BATCH_SEQUENCES 里的计数器:

  { _id: 'BATCH_JOB_INSTANCE_SEQ', count: Long('1') },
  { _id: 'BATCH_JOB_EXECUTION_SEQ', count: Long('2') },
  { _id: 'BATCH_STEP_EXECUTION_SEQ', count: Long('2') }

JobExecution 文档里能看到 jobParameters 数组,以及一份嵌进来的 stepExecutions(BATCH_STEP_EXECUTION 里另外还存了一份):

  jobParameters: [
    {
      name: 'input',
      value: 'customers.csv',
      type: 'java.lang.String',
      identifying: true
    }
  ],
  stepExecutions: [

执行上下文的 key 带点号,比如 customer-reader.read.count。MongoDB 字段名里的点默认会被 Spring Data 拦下来,Batch 文档专门提醒过要给 MongoTemplate 配 map key 的点号替换。Boot 的自动配置在创建 Batch 用的 MongoTemplate 时已经调用了 setMapKeyDotReplacement("."),所以上面看到的 key 原样存了下来。如果你绕开 Boot 自己配,这一步得自己做。

踩到的坑

单机 MongoDB 不行

我另起了一个不带 --replSet 的 mongo 容器,映射到 27018,同样的 jar 指过去:

org.springframework.dao.InvalidDataAccessApiUsageException: Command execution failed on MongoDB server with error 20 (IllegalOperation): 'Transaction numbers are only allowed on a replica set member or mongos' on server localhost:27018. The full response is {"ok": 0.0, "errmsg": "Transaction numbers are only allowed on a replica set member or mongos", "code": 20, "codeName": "IllegalOperation"}

建集合那一步能过,作业一启动就挂。Batch 的 Mongo JobRepository 走 MongoTransactionManager,事务要求副本集或 mongos。本地开发用单节点副本集就够了,记得连接串里带 replicaSet=rs0。

忘开 schema.initialize

换一个空库、把它设成 false,报错不太直观:

java.lang.RuntimeException: Failed to retrieve next value of sequence
Caused by: java.lang.NullPointerException: Cannot invoke "org.bson.Document.getLong(Object)" because the return value of "com.mongodb.client.MongoCollection.findOneAndUpdate(org.bson.conversions.Bson, org.bson.conversions.Bson, com.mongodb.client.model.FindOneAndUpdateOptions)" is null

根子是 BATCH_SEQUENCES 里没有那三条计数器文档。生产库如果不想让应用建集合,就得由 DBA 预先执行 schema-mongodb.jsonl 里的那几条命令。我这个库已经初始化过以后,带着 initialize=true 又启动了两次,都正常,序列值也没被重置。

脚本没建索引

Boot 参考文档说这个开关会初始化 collections and indexes,但 6.0.5 的 schema-mongodb.jsonl 只有建集合和插序列,执行完 BATCH_JOB_EXECUTION 上只有默认的 _id 索引:

$ mongosh batchdemo --eval 'db.BATCH_JOB_EXECUTION.getIndexes().length'
1

Batch 查元数据靠的是 jobInstanceId、jobExecutionId 这些字段,作业历史攒多了以后,最好自己补上索引。

失败执行的 exitDescription 存的是整段堆栈

第一次失败的 JobExecution 里,exitStatus.exitDescription 从 FatalStepExecutionException: Unable to process chunk 一路存到 Caused by: java.lang.IllegalStateException: boom at customer 57,六十多行。失败频繁的作业,文档会越来越胖。

集合前缀现在还配不了

之前聊 Spring Batch 6.1.0-M2 的那篇提到 6.1 给 @EnableMongoJobRepository 加了 collectionPrefix。但 Boot 4.1.1 管理的是 Batch 6.0.5,注解里还没有这个属性:

public interface org.springframework.batch.core.configuration.annotation.EnableMongoJobRepository extends java.lang.annotation.Annotation {
  public abstract java.lang.String mongoOperationsRef();
  public abstract java.lang.String transactionManagerRef();
  public abstract org.springframework.transaction.annotation.Isolation isolationLevelForCreate();
  public abstract boolean validateTransactionState();
  public abstract java.lang.String jobKeyGeneratorRef();
  public abstract java.lang.String jobInstanceIncrementerRef();
  public abstract java.lang.String jobExecutionIncrementerRef();
  public abstract java.lang.String stepExecutionIncrementerRef();
}

Boot 这边 spring.batch.data.mongodb 下也没有前缀属性,集合名固定是 BATCH_ 开头。Spring Batch 6.1 的参考文档已经写了 @EnableMongoJobRepository(collectionPrefix = "MY_APP_") 的用法,在 Boot 升级到 Batch 6.1 之前,几个应用想共用一个 Mongo 只能分库。

参考

文中的运行输出都来自上面这套 Spring Boot 4.1.1 加 MongoDB 8.3 副本集的实测,自动配置的条件和属性对照了 Boot v4.1.1 源码,其余参考 Josh Long 的 MongoDB-backed Spring Batch jobs and more in Spring Boot 4.1、Spring Boot 参考文档的 Spring Batch 章节和 Spring Batch 参考文档 Configuring a JobRepository。

— 感谢阅读 —
广告

一起交流

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