Spring Blog

MongoDB-backed Spring Batch jobs and more in Spring Boot 4.1

8.5内容质量

TL;DR · AI 摘要

Spring Boot 4.1 支持 MongoDB 作为 Spring Batch 的 JobRepository,无需依赖 SQL 数据库。

核心要点

  • Spring Boot 4.1 引入了 `spring-boot-starter-batch-data-mongodb`,支持 MongoDB 作为 Spring Batch 的 JobRepository。
  • Spring Batch 从 JDBC 解耦,使 MongoDB 用户无需使用 SQL 数据库。
  • Spring Boot 4.1 提供了与 JDBC 用户相同的零配置体验,适用于 MongoDB。

结构提纲

按章节快速跳转。

  1. Spring Batch 最初依赖 SQL 数据库,但 Spring Boot 4.1 现在支持 MongoDB

  2. Spring Boot 4.1 引入了 `spring-boot-starter-batch-data-mongodb`,使 MongoDB 成为 JobRepository 的支持选项。

  3. 通过 `docker compose up` 启动 MongoDB 和 PostgreSQL 实例,用于 Spring Batch 的 ETL 任务。

  4. Spring Boot 提供了 `application.properties` 配置 MongoDB 和 PostgreSQL 的连接与初始化。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • Spring Boot 4.1 与 MongoDB 集成
    • Spring Batch 支持 MongoDB
      • JobRepository 支持 MongoDB
      • 无需 SQL 数据库
    • 基础设施设置
      • 使用 Docker Compose 启动 MongoDB 和 PostgreSQL
    • 配置与初始化
      • 通过 `application.properties` 配置 MongoDB 和 PostgreSQL

金句 / Highlights

值得收藏与分享的关键句。

  • Spring Boot 4.1 引入了 `spring-boot-starter-batch-data-mongodb`,使 MongoDB 成为 Spring Batch 的 JobRepository 支持选项。

    第 2 段

    ⬇︎ 下载 PNG𝕏 分享到 X
  • Spring Batch 从 JDBC 解耦,使 MongoDB 用户无需使用 SQL 数据库。

    第 2 段

    ⬇︎ 下载 PNG𝕏 分享到 X
  • Spring Boot 提供了与 JDBC 用户相同的零配置体验,适用于 MongoDB。

    第 3 段

    ⬇︎ 下载 PNG𝕏 分享到 X
#Spring Boot#Spring Batch#MongoDB#Java
打开原文

标题:Spring Boot 4.1 中基于 MongoDB 的 Spring Batch 任务及其他功能

URL 源:https://spring.io/blog/2026/06/21/spring-boot-41-and-spring-batch

Markdown 内容: Spring Batch 在 MongoDB 存在之前就已经被引入,其设计假设存在一个 SQL 数据库,用于存储 Spring Batch 任务的状态。

但那是 _几十年_ 前的事了,对于任何刚开始接触 Spring Batch 的人来说,一个常见问题是,_"为什么这个东西需要和 SQL 数据库通信?"_ 当然,答案是 Spring Batch 会详细记录每个任务、步骤和执行情况到一个 JobRepository 中,多年来这个仓库只使用一种方言:SQL。如果你正愉快地生活在 MongoDB 的世界里,你仍然需要拖着一个 Postgres 或 MySQL 实例,仅仅为了让 Batch 记录它上周二做了什么。

在最近的 Spring Batch 版本中,Spring Batch 将其 JobRepository 与 JDBC 解耦,而 Spring Boot 4.1 最终通过一个合适的 spring-boot-starter-batch-data-mongodb 自动配置,完善了这一体验。你将获得与 JDBC 用户从一开始享受的相同零配置 Boot 体验,用于你的批处理元数据。

有趣的是:Spring Boot 的联合创始人 Dave Syer 博士,曾是 Spring Batch 的创始人和长期负责人。自然地,他为 Spring Boot 编写的第一个自动配置就是为 Spring Batch!所以当我提到 Spring Boot 用户从 _一开始_ 就享受 JDBC 支持时,我是认真的!

本文将逐步介绍一个小型但完整的示例。它包括以下内容:

  • 将 Spring Batch 的 JobRepository 存储在 MongoDB 中(通过新的 4.1 启动器)。
  • 从类路径中读取 customers.csv
  • 将行写入 PostgreSQLcustomers 表中。
  • 使用项目根目录中的 compose.yaml 启动的服务运行所有内容。

搭建基础设施

在我们编写任何一行 Java 代码之前,启动两个支持服务:

code
docker compose up

compose.yaml 启动了一个配置为单节点副本集的 MongoDB 实例(Spring Batch 的 MongoDB 支持需要事务,而事务需要副本集),一个用于目标表的 PostgreSQL 实例,以及一个 Grafana LGTM 容器用于可选的可观测性。

我们将定义一个简单的 ETL(提取、转换、加载)任务,从 customers.csv 文件读取数据,并写入 PostgreSQL 数据库中的一个名为 customers 的表。

我们需要初始化 Postgres 的 customers 表;这由 src/main/resources/schema.sql 处理:

code
create table if not exists customers (
    id    serial primary key,
    name  varchar(255),
    email varchar(255)
);

Spring Boot 的 SQL 初始化(spring.sql.init.mode=always)在启动时运行它。MongoDB 的部分同样可以自行处理 —— spring.batch.data.mongodb.schema.initialize=true 告诉新的启动器创建 JobRepository 所需的集合。

application.properties 中的相关部分:

code
spring.mongodb.host=localhost
spring.mongodb.port=27017
spring.mongodb.database=mydatabase

spring.batch.data.mongodb.schema.initialize=true

spring.datasource.url=jdbc:postgresql://localhost/mydatabase
spring.datasource.username=myuser
spring.datasource.password=secret

我们同时拥有 JDBC 和 MongoDB 的连接。因为我们想要的是基于 MongoDB 的 JobRepository 而不是基于 JDBC 的,所以我们告诉 Spring Boot 的自动配置让步:

code
@SpringBootApplication(exclude = {BatchJdbcAutoConfiguration.class})
public class BatchApplication { ... }

这个单一的排除项将仓库从 JDBC 切换到了 MongoDB。其余的一切 —— MongoDB 客户端、集合、事务管理器 —— 都来自于新的 starter。

任务

我们的任务名为 etl,它按顺序运行两个步骤:

code
@Bean
Job job(@Qualifier(STEP_RESET) Step stepReset,
        @Qualifier(STEP_FILES_TO_DB) Step stepFilesToDb) {
    return new JobBuilder("etl", this.repository)
            .start(stepReset)
            .next(stepFilesToDb)
            .incrementer(new RunIdIncrementer())
            .build();
}

首先执行 reset(一个用于清空目标表的任务),然后是 files-to-db(一个读取器 → 处理器 → 写入器,实际负责数据迁移)。RunIdIncrementer 是一个小但重要的细节:它会在每次启动时增加 run.id 参数,从而使 Spring Batch 将每次调用视为一个新的任务实例,而不是拒绝重新运行一个“已完成”的任务。

第一步:一个任务

Spring Batch 最简单的步骤是 Tasklet —— 一个没有读取或写入项目概念的单一工作块。当你只需要在步骤之间执行某些操作时,这是合适的工具。在这里,它是一个干净的起点:

code
@Bean(STEP_RESET)
Step cleanTableStep(JdbcClient db, JobRepository repository) {
    return new StepBuilder("reset", repository)
            .tasklet((contribution, chunkContext) -> {
                db.sql("delete from customers").update();
                return RepeatStatus.FINISHED;
            })
            .build();
}

任务执行一次,返回 RepeatStatus.FINISHED,然后继续执行下一步。

第二步:读取器、处理器、写入器

有趣的是分块步骤。Spring Batch 的核心模式是 _读取一个项目,处理它,累积一个分块,写入该分块_。读取器从 customers.csv 中提取行:

code
@Bean
FlatFileItemReader<Customer> customerFlatFileItemReader(
        @Value("classpath:/customers.csv") Resource csv) {
    return new FlatFileItemReaderBuilder<Customer>()
            .name("customer-reader")
            .resource(csv)
            .delimited(c -> c.delimiter(",").names("id", "name", "email"))
            .fieldSetMapper(fs -> new Customer(
                    fs.readInt("id"),
                    fs.readString("name"),
                    fs.readString("email")))
            .build();
}

CSV 文件本身:

code
id,name,email
1,josh,josh@joshlong.com
2,dashaun,dashaun@dashaun.com
3,james,james@jamesward.dev

你大概已经明白了。

写入器将每个分块推送到 Postgres,并使用 ON CONFLICT DO NOTHING,这样重新运行时不会因为主键冲突而崩溃:

code
@Bean
JdbcBatchItemWriter<Customer> customerJdbcBatchItemWriter(DataSource dataSource) {
    return new JdbcBatchItemWriterBuilder<Customer>()
            .dataSource(dataSource)
            .sql("INSERT INTO customers(id, name, email) VALUES (:id, :name, :email) on conflict do nothing")
            .itemSqlParameterSourceProvider(item -> new MapSqlParameterSource(
                    Map.of("id", item.id(), "name", item.name(), "email", item.email())))
            .build();
}

并且这一步本身通过一个小型的通过处理器(一个以后添加转换、丰富或过滤逻辑的绝佳位置)将它们连接在一起,块大小为 10:

code
@Bean(STEP_FILES_TO_DB)
Step step(FlatFileItemReader<Customer> reader,
          JdbcBatchItemWriter<Customer> writer) {
    return new StepBuilder("files-to-db", this.repository)
            .<Customer, Customer>chunk(10)
            .reader(reader)
            .processor(customer -> {
                IO.println("processing " + customer);
                return customer;
            })
            .writer(writer)
            .faultTolerant()
            .retryLimit(10)
            .retry(IllegalArgumentException.class)
            .build();
}

注意 faultTolerant() 开关和重试策略 —— Spring Batch 会安静地重试抛出 IllegalArgumentException 的项目,最多十次,然后再失败该块。它是单行代码,因为框架为你处理了所有账务,而这些账务正是现在存储在 MongoDB 中的内容。

运行它

在 Docker Compose 启动后:

code
./mvnw spring-boot:run

任务开始运行,reset 清空 Postgres,files-to-db 将 CSV 通过块流水线传输到 Postgres,每一步的转换、项目数量、退出状态和执行时间戳都会被写入 MongoDB。打开 mongosh,你会看到熟悉的 Batch 集合 —— BATCH_JOB_INSTANCEBATCH_JOB_EXECUTIONBATCH_STEP_EXECUTION —— 只不过它们现在是文档,而不是表。

可观测性

Spring Batch 任务会发出大量有趣的 Spring ApplicationEvent!我监听其中一个事件,JobExecutionEvent,它在任务完成(无论成功与否)时都会发布。

code
@EventListener
    void after(JobExecutionEvent event) {
        IO.println("Job execution #" + event.getJobExecution() + " finished");
    }

当我使用 Spring Initializr 初始化程序时,我确保添加了 OpenTelemetry Spring Boot 启动器。Spring Boot 和 Micrometer 一直支持 OpenTelemetry,但总是需要一些调整。现在,如果你在类路径上有 OpenTelemetry 启动器,它会将指标发布到任何 OpenTelemetry 端点(默认端口为 3000)。如果你在 Spring Initializr 上选择了 Docker Compose 支持,它还会为你提供一个 Grafana 配置,它也会在端口 3000 上监听 OpenTelemetry 信息!

因此,运行应用程序,然后访问 localhost:3000,点击 Drilldown,然后点击 Metrics,在搜索框中搜索 spring_batch

或者,你可以访问 http://localhost:8080/actuator/metrics 查看相同的指标。但我喜欢炫酷和色彩,所以 Grafana 页面对我来说做了很多。

额外奖励:使用 GraalVM 的原生镜像

GraalVM 原生镜像技术有潜力减少整体内存使用。Spring Batch 已经 _大部分_ 可以与 GraalVM 原生镜像一起工作,但有一些新的类是我需要考虑的。还有一些新的模式文件。

code
static class Hints implements RuntimeHintsRegistrar {
code
@Override
public void registerHints(@NonNull RuntimeHints hints, @Nullable ClassLoader classLoader) {
    for (var c : new Class[]{
            org.springframework.batch.core.repository.persistence.JobInstance.class,
            org.springframework.batch.core.repository.persistence.ExecutionContext.class,
            org.springframework.batch.core.repository.persistence.ExitStatus.class,
            org.springframework.batch.core.repository.persistence.StepExecution.class,
            org.springframework.batch.core.repository.persistence.JobExecution.class,
            org.springframework.batch.core.repository.persistence.JobParameter.class,
    }) {
        hints.reflection().registerType(c, MemberCategory.values());
    }

    var prefix = "org/springframework/batch/core/";
    for (var r : new String[]{
            "schema-mongodb", //
            "schema-drop-mongodb"}) {
        for (var suffix : "jsonl,js".split(",")) {
            var path = prefix + r + "." + suffix;
            var resource = new ClassPathResource(path);
            if (resource.exists()) {
                hints.resources().registerResource(resource);
            }
        }
    }
}

我们还需要告诉 GraalVM 关于 customers.csv 的信息。

code
static class ResourceHints implements RuntimeHintsRegistrar {

        @Override
        public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
            hints.resources().registerResource(new ClassPathResource("/customers.csv"));
        }
    }

通过将以下内容添加到 BatchConfiguration 类(或任何带有 @Configuration 注解的类)中,以常规方式注册这两个类:

code
@ImportRuntimeHints({BatchConfiguration.ResourceHints.class, BatchConfiguration.Hints.class})

完成这些操作后,就可以以常规方式构建 GraalVM 原生镜像。我在仓库根目录的 native.sh 脚本中写下了步骤:

code
#!/usr/bin/env bash
ls -la target && rm -rf target
./mvnw -DskipTests -Pnative native:compile
./target/batch

运行应用程序:./target/batch,并观察到它启动速度非常快,使用的内存远少于在 JVM 上运行时。在我的机器上,这是一台搭载 Apple M5 和 macOS 的设备,它大约在十分之一秒内启动,并使用大约 150 MB 的内存。长时间运行的批处理任务通常不需要快速启动,但内存的节省是很有帮助的,而且启动时间也不会造成任何问题!

延迟的 DataSource 连接

另一个优化,虽然在整体上对这个特定工作负载的改变不大,但非常有用,那就是在 Spring Boot 4.1 中我们现在支持 _延迟连接获取_。请记住,默认情况下,Spring Boot 会在事务开始时初始化 DataSource 并创建连接,即使没有保证你将会使用该连接。你可以通过新的 Spring Boot 配置属性 spring.datasource.connection-fetch=lazy 来避免这种代价。

获取相关资源

如往常一样,本示例的完整代码可在此处在线获取。

[为什么这很重要](https://spring.io/blog/2026/06/21/spring-boot-41-and-spring-batch#why-this-matters)

Spring Batch 与关系数据库之间的历史耦合一直是一种务实的妥协,而不是设计的理想。框架需要一个 _某处_ 可持久化的地方来记住它做了什么,而 SQL 是最简单的路径。现在 JobRepository 抽象已经正确解耦 —— 并且 Spring Boot 4.1 提供了 MongoDB 的一流自动配置 —— 使用文档存储的团队不再需要仅仅为了迎合批处理层级而保留一个 JDBC 数据库。

选择适合你数据的数据库。Spring Batch 会根据你的选择进行适配。