MongoDB-backed Spring Batch jobs and more in Spring Boot 4.1
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。
结构提纲
按章节快速跳转。
- §引言
Spring Batch 最初依赖 SQL 数据库,但 Spring Boot 4.1 现在支持 MongoDB。
Spring Boot 4.1 引入了 `spring-boot-starter-batch-data-mongodb`,使 MongoDB 成为 JobRepository 的支持选项。
通过 `docker compose up` 启动 MongoDB 和 PostgreSQL 实例,用于 Spring Batch 的 ETL 任务。
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 支持选项。
Spring Batch 从 JDBC 解耦,使 MongoDB 用户无需使用 SQL 数据库。
Spring Boot 提供了与 JDBC 用户相同的零配置体验,适用于 MongoDB。
标题: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。 - 将行写入 PostgreSQL 的
customers表中。 - 使用项目根目录中的
compose.yaml启动的服务运行所有内容。
搭建基础设施
在我们编写任何一行 Java 代码之前,启动两个支持服务:
docker compose upcompose.yaml 启动了一个配置为单节点副本集的 MongoDB 实例(Spring Batch 的 MongoDB 支持需要事务,而事务需要副本集),一个用于目标表的 PostgreSQL 实例,以及一个 Grafana LGTM 容器用于可选的可观测性。
我们将定义一个简单的 ETL(提取、转换、加载)任务,从 customers.csv 文件读取数据,并写入 PostgreSQL 数据库中的一个名为 customers 的表。
我们需要初始化 Postgres 的 customers 表;这由 src/main/resources/schema.sql 处理:
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 中的相关部分:
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 的自动配置让步:
@SpringBootApplication(exclude = {BatchJdbcAutoConfiguration.class})
public class BatchApplication { ... }这个单一的排除项将仓库从 JDBC 切换到了 MongoDB。其余的一切 —— MongoDB 客户端、集合、事务管理器 —— 都来自于新的 starter。
任务
我们的任务名为 etl,它按顺序运行两个步骤:
@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 —— 一个没有读取或写入项目概念的单一工作块。当你只需要在步骤之间执行某些操作时,这是合适的工具。在这里,它是一个干净的起点:
@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 中提取行:
@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 文件本身:
id,name,email
1,josh,josh@joshlong.com
2,dashaun,dashaun@dashaun.com
3,james,james@jamesward.dev你大概已经明白了。
写入器将每个分块推送到 Postgres,并使用 ON CONFLICT DO NOTHING,这样重新运行时不会因为主键冲突而崩溃:
@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:
@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 启动后:
./mvnw spring-boot:run任务开始运行,reset 清空 Postgres,files-to-db 将 CSV 通过块流水线传输到 Postgres,每一步的转换、项目数量、退出状态和执行时间戳都会被写入 MongoDB。打开 mongosh,你会看到熟悉的 Batch 集合 —— BATCH_JOB_INSTANCE、BATCH_JOB_EXECUTION、BATCH_STEP_EXECUTION —— 只不过它们现在是文档,而不是表。
可观测性
Spring Batch 任务会发出大量有趣的 Spring ApplicationEvent!我监听其中一个事件,JobExecutionEvent,它在任务完成(无论成功与否)时都会发布。
@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 原生镜像一起工作,但有一些新的类是我需要考虑的。还有一些新的模式文件。
static class Hints implements RuntimeHintsRegistrar {
@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 的信息。
static class ResourceHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
hints.resources().registerResource(new ClassPathResource("/customers.csv"));
}
}通过将以下内容添加到 BatchConfiguration 类(或任何带有 @Configuration 注解的类)中,以常规方式注册这两个类:
@ImportRuntimeHints({BatchConfiguration.ResourceHints.class, BatchConfiguration.Hints.class})完成这些操作后,就可以以常规方式构建 GraalVM 原生镜像。我在仓库根目录的 native.sh 脚本中写下了步骤:
#!/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 会根据你的选择进行适配。