Add support for Spring Batch MongoDB

This commit adds support for running Spring Batch jobs with a MongoDB
store. It aligns as much as possible to the JDBC counterpart, with
spring-boot-starter-batch-data-mongodb and spring-boot-starter-batch-data-mongodb-test.

As we do not have a way to initialize a MongoDB store at the moment,
this commit adds a conservative approach of executing commands defined
by the standard Spring Batch schema script.

Closes gh-43236
This commit is contained in:
Stéphane Nicoll
2026-03-16 12:11:19 +01:00
parent f2981d1bdc
commit b12d40e8e5
21 changed files with 1077 additions and 2 deletions
@@ -117,6 +117,7 @@ apiref-openjdk=https://docs.oracle.com/en/java/javase/17/docs/api
code-spring-boot=https://github.com/{github-repo}/tree/{github-ref}
code-spring-boot-autoconfigure-src={code-spring-boot}/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure
code-spring-boot-batch-data-mongodb-src={code-spring-boot}/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb
code-spring-boot-batch-jdbc-src={code-spring-boot}/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc
code-spring-boot-batch-src={code-spring-boot}/module/spring-boot-batch/src/main/java/org/springframework/boot/batch
code-spring-boot-freemarker-src={code-spring-boot}/module/spring-boot-freemarker/src/main/java/org/springframework/boot/freemarker
@@ -235,6 +235,7 @@ dependencies {
javadocMacros(project(":module:spring-boot-activemq"))
javadocMacros(project(":module:spring-boot-artemis"))
javadocMacros(project(":module:spring-boot-batch"))
javadocMacros(project(":module:spring-boot-batch-data-mongodb"))
javadocMacros(project(":module:spring-boot-batch-jdbc"))
javadocMacros(project(":module:spring-boot-cache-test"))
javadocMacros(project(":module:spring-boot-data-r2dbc-test"))
@@ -7,6 +7,7 @@ When building a batch application, the following stores can be auto-configured:
* In-memory
* JDBC
* MongoDB
Each store has specific additional settings.
For instance, it is possible to customize the tables prefix for the JDBC store, as shown in the following example:
@@ -19,8 +20,20 @@ spring:
table-prefix: "CUSTOM_"
----
When using the MongoDB store, you can enable initialization of the Spring Batch job repository schema (collections and indexes):
[configprops,yaml]
----
spring:
batch:
data:
mongodb:
schema:
initialize: true
----
To disable Spring Boot's auto-configuration and take complete control of Spring Batch's configuration, add javadoc:org.springframework.batch.core.configuration.annotation.EnableBatchProcessing[format=annotation] to one of your javadoc:org.springframework.context.annotation.Configuration[format=annotation] classes or extend javadoc:org.springframework.batch.core.configuration.support.DefaultBatchConfiguration[].
This will cause the auto-configuration to back off, including initialization of Spring Batch's database schema if you're using the JDBC-based store.
This will cause the auto-configuration to back off, including initialization of Spring Batch's database schema (JDBC or MongoDB).
Spring Batch can then be configured using the `@Enable*JobRepository` annotation's attributes rather than the previously described configuration properties.
To learn more about manually configuring Spring Batch, see the API documentation of:
@@ -50,4 +63,4 @@ spring:
enabled: false
----
See {code-spring-boot-batch-src}/autoconfigure/BatchAutoConfiguration.java[`BatchAutoConfiguration`] and {code-spring-boot-batch-jdbc-src}/autoconfigure/BatchJdbcAutoConfiguration.java[`BatchJdbcAutoConfiguration`] for more details.
See {code-spring-boot-batch-src}/autoconfigure/BatchAutoConfiguration.java[`BatchAutoConfiguration`], {code-spring-boot-batch-jdbc-src}/autoconfigure/BatchJdbcAutoConfiguration.java[`BatchJdbcAutoConfiguration`], and {code-spring-boot-batch-data-mongodb-src}/autoconfigure/BatchDataMongoAutoConfiguration.java[`BatchDataMongoAutoConfiguration`] for more details.
@@ -0,0 +1,45 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the License);
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id "java-library"
id "org.springframework.boot.auto-configuration"
id "org.springframework.boot.configuration-properties"
id "org.springframework.boot.deployed"
id "org.springframework.boot.docker-test"
id "org.springframework.boot.optional-dependencies"
}
description = "Spring Boot Batch Data MongoDB"
dependencies {
api(project(":module:spring-boot-batch"))
api(project(":module:spring-boot-data-mongodb"))
optional(project(":core:spring-boot-autoconfigure"))
optional("org.mongodb:mongodb-driver-sync")
dockerTestImplementation(project(":core:spring-boot-test"))
dockerTestImplementation(project(":test-support:spring-boot-docker-test-support"))
dockerTestImplementation("org.mongodb:mongodb-driver-sync")
dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter")
dockerTestImplementation("org.testcontainers:testcontainers-mongodb")
testImplementation(project(":core:spring-boot-test"))
testImplementation(project(":test-support:spring-boot-test-support"))
testRuntimeOnly("ch.qos.logback:logback-classic")
}
@@ -0,0 +1,98 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.batch.mongodb.autoconfigure;
import java.time.LocalDateTime;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.mongodb.MongoDBContainer;
import org.springframework.batch.core.job.Job;
import org.springframework.batch.core.job.JobExecution;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.job.parameters.JobParameters;
import org.springframework.batch.core.job.parameters.JobParametersBuilder;
import org.springframework.batch.core.launch.JobOperator;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.infrastructure.repeat.RepeatStatus;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.data.mongodb.autoconfigure.DataMongoAutoConfiguration;
import org.springframework.boot.mongodb.autoconfigure.MongoAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.testsupport.container.TestImage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoOperations;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link BatchDataMongoAutoConfiguration}.
*
* @author Stephane Nicoll
*/
@Testcontainers(disabledWithoutDocker = true)
class BatchDataMongoAutoConfigurationIntegrationTests {
@Container
static final MongoDBContainer mongoDb = TestImage.container(MongoDBContainer.class).withReplicaSet();
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withUserConfiguration(JobConfiguration.class)
.withPropertyValues("spring.batch.data.mongodb.schema.initialize=true",
"spring.mongodb.uri=" + mongoDb.getReplicaSetUrl())
.withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class, DataMongoAutoConfiguration.class,
BatchDataMongoAutoConfiguration.class));
@Test
void runJob() {
this.contextRunner.withUserConfiguration(JobConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(JobOperator.class)
.hasSingleBean(JobRepository.class)
.hasSingleBean(Job.class);
JobParameters jobParameters = new JobParametersBuilder().addString("name", "foo")
.addLocalDateTime("runtime", LocalDateTime.now())
.toJobParameters();
JobExecution jobExecution = context.getBean(JobOperator.class)
.start(context.getBean(Job.class), jobParameters);
assertThat(jobExecution).isNotNull();
assertThat(context.getBean(JobRepository.class).getLastJobExecution("job", jobParameters)).isNotNull();
assertThat(context.getBean(MongoOperations.class).getCollection("BATCH_JOB_EXECUTION").countDocuments())
.isPositive();
});
}
@Configuration(proxyBeanMethods = false)
static class JobConfiguration {
@Bean
Job job(JobRepository jobRepository) {
return new JobBuilder("job", jobRepository)
.start(new StepBuilder("step1", jobRepository)
.tasklet((contribution, chunkContext) -> RepeatStatus.FINISHED)
.build())
.build();
}
}
}
@@ -0,0 +1,149 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.batch.mongodb.autoconfigure;
import com.mongodb.client.MongoClient;
import org.jspecify.annotations.Nullable;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration;
import org.springframework.batch.core.configuration.support.MongoDefaultBatchConfiguration;
import org.springframework.batch.core.launch.JobOperator;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.batch.autoconfigure.BatchAutoConfiguration;
import org.springframework.boot.batch.autoconfigure.BatchJobLauncherAutoConfiguration;
import org.springframework.boot.batch.autoconfigure.BatchTaskExecutor;
import org.springframework.boot.batch.autoconfigure.BatchTransactionManager;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
import org.springframework.boot.data.mongodb.autoconfigure.DataMongoAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.task.TaskExecutor;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.MongoTransactionManager;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.transaction.annotation.Isolation;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Spring Batch using Data MongoDB.
*
* @author Stephane Nicoll
* @since 4.1.0
*/
@AutoConfiguration(before = { BatchAutoConfiguration.class, BatchJobLauncherAutoConfiguration.class },
after = DataMongoAutoConfiguration.class)
@ConditionalOnClass({ JobOperator.class, MongoClient.class, MongoOperations.class })
@ConditionalOnBean(MongoDatabaseFactory.class)
@ConditionalOnMissingBean(value = DefaultBatchConfiguration.class, annotation = EnableBatchProcessing.class)
@EnableConfigurationProperties(BatchDataMongoProperties.class)
public final class BatchDataMongoAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty("spring.batch.data.mongodb.schema.initialize")
@ConditionalOnBean(MongoOperations.class)
@Import(JobRepositoryDependsOnSchemaInitializationDetector.class)
static class BatchMongoDatabaseInitializerConfiguration {
@Bean(name = JobRepositoryDependsOnSchemaInitializationDetector.SCHEMA_INITIALIZATION_BEAN_NAME)
InitializingBean batchMongoDataInitializingBean(MongoOperations mongoOperations,
BatchDataMongoProperties properties) {
return () -> {
BatchMongoSchemaInitializer initializer = new BatchMongoSchemaInitializer(mongoOperations);
String schemaLocation = properties.getSchema().getLocation();
Resource resource = new ClassPathResource(schemaLocation);
if (!resource.exists()) {
throw new InvalidConfigurationPropertyValueException("spring.batch.data.mongodb.schema.location",
schemaLocation, "resource does not exist");
}
initializer.initialize(resource);
};
}
}
@Configuration(proxyBeanMethods = false)
static class SpringBootBatchMongoConfiguration extends MongoDefaultBatchConfiguration {
private final MongoOperations mongoOperations;
private final MongoTransactionManager transactionManager;
private final @Nullable TaskExecutor taskExecutor;
private final BatchDataMongoProperties properties;
SpringBootBatchMongoConfiguration(MongoDatabaseFactory mongoDatabaseFactory,
ObjectProvider<MongoTransactionManager> transactionManager,
@BatchTransactionManager ObjectProvider<MongoTransactionManager> batchTransactionManager,
@BatchTaskExecutor ObjectProvider<TaskExecutor> batchTaskExecutor,
BatchDataMongoProperties properties) {
this.mongoOperations = createMongoOperations(mongoDatabaseFactory);
this.transactionManager = batchTransactionManager.getIfAvailable(
() -> transactionManager.getIfAvailable(() -> new MongoTransactionManager(mongoDatabaseFactory)));
this.taskExecutor = batchTaskExecutor.getIfAvailable();
this.properties = properties;
}
private static MongoTemplate createMongoOperations(MongoDatabaseFactory mongoDatabaseFactory) {
MongoTemplate template = new MongoTemplate(mongoDatabaseFactory);
MappingMongoConverter converter = (MappingMongoConverter) template.getConverter();
converter.setMapKeyDotReplacement(".");
return template;
}
@Override
protected MongoOperations getMongoOperations() {
return this.mongoOperations;
}
@Override
protected MongoTransactionManager getTransactionManager() {
return this.transactionManager;
}
@Override
protected boolean getValidateTransactionState() {
return this.properties.isValidateTransactionState();
}
@Override
protected Isolation getIsolationLevelForCreate() {
Isolation isolation = this.properties.getIsolationLevelForCreate();
return (isolation != null) ? isolation : super.getIsolationLevelForCreate();
}
@Override
protected TaskExecutor getTaskExecutor() {
return (this.taskExecutor != null) ? this.taskExecutor : super.getTaskExecutor();
}
}
}
@@ -0,0 +1,96 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.batch.mongodb.autoconfigure;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.transaction.annotation.Isolation;
/**
* Configuration properties for Spring Batch using Data MongoDB.
*
* @author Stephane Nicoll
* @since 4.1.0
*/
@ConfigurationProperties("spring.batch.data.mongodb")
public class BatchDataMongoProperties {
/**
* Whether to validate the transaction state.
*/
private boolean validateTransactionState = true;
/**
* Transaction isolation level to use when creating job metadata for new jobs.
*/
private @Nullable Isolation isolationLevelForCreate;
private final Schema schema = new Schema();
public boolean isValidateTransactionState() {
return this.validateTransactionState;
}
public void setValidateTransactionState(boolean validateTransactionState) {
this.validateTransactionState = validateTransactionState;
}
public @Nullable Isolation getIsolationLevelForCreate() {
return this.isolationLevelForCreate;
}
public void setIsolationLevelForCreate(@Nullable Isolation isolationLevelForCreate) {
this.isolationLevelForCreate = isolationLevelForCreate;
}
public Schema getSchema() {
return this.schema;
}
public static class Schema {
/**
* Path to the newline-delimited JSON script used to create the Spring Batch job
* repository collections and indexes in MongoDB.
*/
private String location = "org/springframework/batch/core/schema-mongodb.jsonl";
/**
* Whether to initialize the Spring Batch job repository schema in MongoDB.
*/
private boolean initialize;
public String getLocation() {
return this.location;
}
public void setLocation(String location) {
this.location = location;
}
public boolean isInitialize() {
return this.initialize;
}
public void setInitialize(boolean initialize) {
this.initialize = initialize;
}
}
}
@@ -0,0 +1,35 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.batch.mongodb.autoconfigure;
import org.jspecify.annotations.Nullable;
import org.springframework.dao.DataAccessException;
/**
* Thrown when the Spring Batch repository schema cannot be initialized.
*
* @author Stephane Nicoll
* @since 4.1.0
*/
public class BatchMongoSchemaInitializationException extends DataAccessException {
public BatchMongoSchemaInitializationException(String msg, @Nullable Throwable cause) {
super(msg, cause);
}
}
@@ -0,0 +1,69 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.batch.mongodb.autoconfigure;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.Resource;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.util.StreamUtils;
/**
* Initializes the Spring Batch job repository schema (collections and indexes) in MongoDB
* by executing commands from a newline-delimited JSON script.
*
* @author Stephane Nicoll
*/
class BatchMongoSchemaInitializer {
private static final Log logger = LogFactory.getLog(BatchMongoSchemaInitializer.class);
private final MongoOperations mongoOperations;
BatchMongoSchemaInitializer(MongoOperations mongoOperations) {
this.mongoOperations = mongoOperations;
}
void initialize(Resource schema) throws IOException {
StreamUtils.copyToString(schema.getInputStream(), StandardCharsets.UTF_8)
.lines()
.filter((line) -> !line.isBlank())
.forEach((command) -> {
try {
executeCommand(command);
}
catch (Exception ex) {
throw new BatchMongoSchemaInitializationException(
"Failed to initialize Batch repository schema using '%s', command failed: %s"
.formatted(schema, command),
ex);
}
});
}
private void executeCommand(String command) {
if (logger.isTraceEnabled()) {
logger.trace("Executing command: " + command);
}
this.mongoOperations.executeCommand(command);
}
}
@@ -0,0 +1,39 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.batch.mongodb.autoconfigure;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.boot.autoconfigure.AbstractDependsOnBeanFactoryPostProcessor;
/**
* {@link AbstractDependsOnBeanFactoryPostProcessor} implementation that makes sure Spring
* Batch's {@link JobRepository} depends on schema initialization.
*
* @author Stephane Nicoll
*/
class JobRepositoryDependsOnSchemaInitializationDetector extends AbstractDependsOnBeanFactoryPostProcessor {
/**
* Bean name responsible for schema initialization.
*/
static final String SCHEMA_INITIALIZATION_BEAN_NAME = "batchMongoDataInitializingBean";
JobRepositoryDependsOnSchemaInitializationDetector() {
super(JobRepository.class, SCHEMA_INITIALIZATION_BEAN_NAME);
}
}
@@ -0,0 +1,23 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Auto-configuration for Spring Batch with Data MongoDB.
*/
@NullMarked
package org.springframework.boot.batch.mongodb.autoconfigure;
import org.jspecify.annotations.NullMarked;
@@ -0,0 +1 @@
org.springframework.boot.batch.mongodb.autoconfigure.BatchDataMongoAutoConfiguration
@@ -0,0 +1,210 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.batch.mongodb.autoconfigure;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration;
import org.springframework.batch.core.launch.JobOperator;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.batch.autoconfigure.BatchTransactionManager;
import org.springframework.boot.batch.mongodb.autoconfigure.BatchDataMongoAutoConfiguration.SpringBootBatchMongoConfiguration;
import org.springframework.boot.data.mongodb.autoconfigure.DataMongoAutoConfiguration;
import org.springframework.boot.mongodb.autoconfigure.MongoAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.MongoTransactionManager;
import org.springframework.data.mongodb.core.MongoExceptionTranslator;
import org.springframework.data.mongodb.core.MongoOperations;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
/**
* Tests for {@link BatchDataMongoAutoConfiguration}.
*
* @author Stephane Nicoll
*/
class BatchDataMongoAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(BatchDataMongoAutoConfiguration.class));
@Test
void autoConfigurationWithSpringDataMongoDb() {
this.contextRunner
.withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class, DataMongoAutoConfiguration.class))
.run((context) -> assertThat(context).hasSingleBean(JobRepository.class).hasSingleBean(JobOperator.class));
}
@Test
void autoConfigurationOnlyRequiresMongoDatabaseFactory() {
this.contextRunner.withBean(MongoDatabaseFactory.class, this::mockMongoDatabaseFactory)
.run((context) -> assertThat(context).hasSingleBean(JobRepository.class).hasSingleBean(JobOperator.class));
}
@Test
void autConfigurationUsesMainTransactionManager() {
MongoTransactionManager transactionManager = mock(MongoTransactionManager.class);
this.contextRunner.withBean(MongoDatabaseFactory.class, this::mockMongoDatabaseFactory)
.withBean(MongoTransactionManager.class, () -> transactionManager)
.run((context) -> assertThat(
context.getBean(SpringBootBatchMongoConfiguration.class).getTransactionManager())
.isSameAs(transactionManager));
}
@Test
void autConfigurationFavorsBatchTransactionManager() {
MongoTransactionManager transactionManager = mock(MongoTransactionManager.class);
this.contextRunner.withBean(MongoDatabaseFactory.class, this::mockMongoDatabaseFactory)
.withBean(MongoTransactionManager.class, () -> transactionManager)
.withUserConfiguration(BatchTransactionManagerConfiguration.class)
.run((context) -> {
assertThat(context.getBeansOfType(MongoTransactionManager.class)).hasSize(2);
assertThat(context.getBean(SpringBootBatchMongoConfiguration.class).getTransactionManager())
.isSameAs(context.getBean("customTransactionManager"));
});
}
@Test
void autConfigurationCreatesBatchTransactionManagerIfNecessary() {
MongoDatabaseFactory mongoDatabaseFactory = mockMongoDatabaseFactory();
this.contextRunner.withBean(MongoDatabaseFactory.class, () -> mongoDatabaseFactory).run((context) -> {
assertThat(context).doesNotHaveBean(MongoTransactionManager.class);
assertThat(context.getBean(SpringBootBatchMongoConfiguration.class).getTransactionManager())
.satisfies((mongoTransactionManager) -> assertThat(mongoTransactionManager.getDatabaseFactory())
.isSameAs(mongoDatabaseFactory));
});
}
@Test
void autoconfigurationBacksOffEntirelyIfSpringMongoDbAbsent() {
this.contextRunner
.withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class, DataMongoAutoConfiguration.class))
.withClassLoader(new FilteredClassLoader(MongoOperations.class))
.run((context) -> assertThat(context).doesNotHaveBean(JobRepository.class)
.doesNotHaveBean(JobOperator.class));
}
@Test
void autoConfigurationBacksOfEntirelyIfSpringMongoDbIsNotConfigured() {
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(JobRepository.class)
.doesNotHaveBean(JobOperator.class));
}
@Test
void autoConfigurationBacksOffWhenUserEnablesBatchProcessing() {
this.contextRunner.withBean(MongoDatabaseFactory.class, this::mockMongoDatabaseFactory)
.withUserConfiguration(EnableBatchProcessingConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean(SpringBootBatchMongoConfiguration.class));
}
@Test
void autoConfigurationBacksOffWhenUserProvidesBatchConfiguration() {
this.contextRunner.withBean(MongoDatabaseFactory.class, this::mockMongoDatabaseFactory)
.withUserConfiguration(CustomBatchConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean(SpringBootBatchMongoConfiguration.class));
}
@Test
void schemaInitializerBeanNotCreatedByDefault() {
this.contextRunner.withBean(MongoDatabaseFactory.class, this::mockMongoDatabaseFactory)
.run((context) -> assertThat(context)
.doesNotHaveBean(JobRepositoryDependsOnSchemaInitializationDetector.SCHEMA_INITIALIZATION_BEAN_NAME));
}
@Test
void schemaInitializerBeanCreatedWhenSchemaInitializeEnabled() {
MongoOperations mongoOperations = mock(MongoOperations.class);
this.contextRunner.withBean(MongoDatabaseFactory.class, this::mockMongoDatabaseFactory)
.withBean(MongoOperations.class, () -> mongoOperations)
.withPropertyValues("spring.batch.data.mongodb.schema.initialize=true")
.run((context) -> {
assertThat(context)
.hasBean(JobRepositoryDependsOnSchemaInitializationDetector.SCHEMA_INITIALIZATION_BEAN_NAME);
// see org/springframework/batch/core/schema-mongodb.jsonl
then(mongoOperations).should(times(7)).executeCommand(anyString());
});
}
@Test
void jobRepositoryDependsOnSchemaInitializerWhenSchemaInitializationEnabled() {
this.contextRunner.withBean(MongoDatabaseFactory.class, this::mockMongoDatabaseFactory)
.withBean(MongoOperations.class, Mockito::mock)
.withPropertyValues("spring.batch.data.mongodb.schema.initialize=true")
.run((context) -> {
assertThat(context).hasSingleBean(JobRepository.class);
BeanDefinition jobRepositoryDefinition = context.getBeanFactory().getBeanDefinition("jobRepository");
assertThat(jobRepositoryDefinition.getDependsOn())
.contains(JobRepositoryDependsOnSchemaInitializationDetector.SCHEMA_INITIALIZATION_BEAN_NAME);
});
}
@Test
void schemaInitializationFailsWhenSchemaLocationDoesNotExist() {
this.contextRunner.withBean(MongoDatabaseFactory.class, this::mockMongoDatabaseFactory)
.withBean(MongoOperations.class, Mockito::mock)
.withPropertyValues("spring.batch.data.mongodb.schema.initialize=true",
"spring.batch.data.mongodb.schema.location=classpath:does/not/exist.jsonl")
.run((context) -> assertThat(context).getFailure()
.hasRootCauseInstanceOf(
org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException.class)
.rootCause()
.hasMessageContaining("spring.batch.data.mongodb.schema.location")
.hasMessageContaining("resource does not exist"));
}
private MongoDatabaseFactory mockMongoDatabaseFactory() {
MongoDatabaseFactory factory = mock(MongoDatabaseFactory.class);
given(factory.getExceptionTranslator()).willReturn(MongoExceptionTranslator.DEFAULT_EXCEPTION_TRANSLATOR);
return factory;
}
@EnableBatchProcessing
@Configuration(proxyBeanMethods = false)
static class EnableBatchProcessingConfiguration {
}
@Configuration(proxyBeanMethods = false)
static class CustomBatchConfiguration extends DefaultBatchConfiguration {
}
@Configuration(proxyBeanMethods = false)
static class BatchTransactionManagerConfiguration {
@Bean
@BatchTransactionManager
MongoTransactionManager customTransactionManager() {
return mock(MongoTransactionManager.class);
}
}
}
@@ -0,0 +1,84 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.batch.mongodb.autoconfigure;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.data.mongodb.core.MongoOperations;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link BatchMongoSchemaInitializer}.
*
* @author Stephane Nicoll
*/
class BatchMongoSchemaInitializerTests {
@Test
void initializeExecutesEachNonBlankLine() throws IOException {
MongoOperations mongoOperations = mock(MongoOperations.class);
BatchMongoSchemaInitializer initializer = new BatchMongoSchemaInitializer(mongoOperations);
Resource schema = resource("{\"create\": \"batch_job_instance\"}\n{\"create\": \"batch_job_execution\"}");
initializer.initialize(schema);
then(mongoOperations).should().executeCommand("{\"create\": \"batch_job_instance\"}");
then(mongoOperations).should().executeCommand("{\"create\": \"batch_job_execution\"}");
}
@Test
void initializeSkipsBlankLines() throws IOException {
MongoOperations mongoOperations = mock(MongoOperations.class);
BatchMongoSchemaInitializer initializer = new BatchMongoSchemaInitializer(mongoOperations);
Resource schema = resource("{\"create\": \"col1\"}\n\n \n{\"create\": \"col2\"}");
initializer.initialize(schema);
then(mongoOperations).should().executeCommand("{\"create\": \"col1\"}");
then(mongoOperations).should().executeCommand("{\"create\": \"col2\"}");
}
@Test
void initializeThrowsWhenCommandFails() {
MongoOperations mongoOperations = mock(MongoOperations.class);
given(mongoOperations.executeCommand(anyString())).willThrow(new RuntimeException("Command failed"));
BatchMongoSchemaInitializer initializer = new BatchMongoSchemaInitializer(mongoOperations);
Resource schema = resource("{\"create\": \"batch_job_instance\"}");
assertThatExceptionOfType(BatchMongoSchemaInitializationException.class)
.isThrownBy(() -> initializer.initialize(schema))
.withMessageContaining("Failed to initialize Batch repository schema")
.withMessageContaining("{\"create\": \"batch_job_instance\"}")
.withCauseInstanceOf(RuntimeException.class);
}
private static Resource resource(String content) {
return new InputStreamResource(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)));
}
}
+4
View File
@@ -85,6 +85,7 @@ include "module:spring-boot-artemis"
include "module:spring-boot-autoconfigure-classic"
include "module:spring-boot-autoconfigure-classic-modules"
include "module:spring-boot-batch"
include "module:spring-boot-batch-data-mongodb"
include "module:spring-boot-batch-jdbc"
include "module:spring-boot-cache"
include "module:spring-boot-cache-test"
@@ -214,6 +215,8 @@ include "starter:spring-boot-starter-artemis-test"
include "starter:spring-boot-starter-aspectj"
include "starter:spring-boot-starter-aspectj-test"
include "starter:spring-boot-starter-batch"
include "starter:spring-boot-starter-batch-data-mongodb"
include "starter:spring-boot-starter-batch-data-mongodb-test"
include "starter:spring-boot-starter-batch-jdbc"
include "starter:spring-boot-starter-batch-jdbc-test"
include "starter:spring-boot-starter-batch-test"
@@ -396,6 +399,7 @@ include ":smoke-test:spring-boot-smoke-test-artemis"
include ":smoke-test:spring-boot-smoke-test-aspectj"
include ":smoke-test:spring-boot-smoke-test-autoconfigure-classic"
include ":smoke-test:spring-boot-smoke-test-batch"
include ":smoke-test:spring-boot-smoke-test-batch-data-mongodb"
include ":smoke-test:spring-boot-smoke-test-batch-jdbc"
include ":smoke-test:spring-boot-smoke-test-bootstrap-registry"
include ":smoke-test:spring-boot-smoke-test-cache"
@@ -0,0 +1,34 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the License);
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id "java"
}
description = "Spring Boot Batch with Data MongoDB smoke test"
dependencies {
implementation(project(":starter:spring-boot-starter-batch-data-mongodb"))
testImplementation(project(":starter:spring-boot-starter-batch-data-mongodb-test"))
testImplementation(project(":test-support:spring-boot-docker-test-support"))
testImplementation("org.testcontainers:testcontainers-junit-jupiter")
testImplementation("org.testcontainers:testcontainers-mongodb")
}
tasks.named("compileTestJava") {
options.nullability.checking = "tests"
}
@@ -0,0 +1,46 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.batch;
import org.springframework.batch.core.job.Job;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.infrastructure.repeat.RepeatStatus;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class SampleBatchApplication {
@Bean
Job job(JobRepository jobRepository) {
return new JobBuilder("job", jobRepository)
.start(new StepBuilder("step1", jobRepository)
.tasklet((contribution, chunkContext) -> RepeatStatus.FINISHED)
.build())
.build();
}
public static void main(String[] args) {
// System.exit is common for Batch applications since the exit code can be used to
// drive a workflow
System.exit(SpringApplication.exit(SpringApplication.run(SampleBatchApplication.class, args)));
}
}
@@ -0,0 +1,20 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@NullMarked
package smoketest.batch;
import org.jspecify.annotations.NullMarked;
@@ -0,0 +1,53 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.batch;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.mongodb.MongoDBContainer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.boot.testsupport.container.TestImage;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Smoke tests for Spring Batch with MongoDB.
*
* @author Stephane Nicoll
*/
@ExtendWith(OutputCaptureExtension.class)
@Testcontainers(disabledWithoutDocker = true)
class SampleBatchApplicationTests {
@Container
static final MongoDBContainer mongoDb = TestImage.container(MongoDBContainer.class).withReplicaSet();
@Test
void testDefaultSettings(CapturedOutput output) {
int exitCode = SpringApplication.exit(SpringApplication.run(SampleBatchApplication.class,
"--spring.mongodb.uri=" + mongoDb.getReplicaSetUrl(),
"--spring.batch.data.mongodb.schema.initialize=true"));
assertThat(exitCode).isZero();
assertThat(output).contains("completed with the following parameters");
}
}
@@ -0,0 +1,26 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id "org.springframework.boot.starter"
}
description = "Starter for testing using Spring Batch with Data MongoDB"
dependencies {
api(project(":starter:spring-boot-starter-batch-test"))
api(project(":starter:spring-boot-starter-data-mongodb-test"))
}
@@ -0,0 +1,28 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id "org.springframework.boot.starter"
}
description = "Starter for using Spring Batch with Data MongoDB"
dependencies {
api(project(":starter:spring-boot-starter-batch"))
api(project(":starter:spring-boot-starter-data-mongodb"))
api(project(":module:spring-boot-batch-data-mongodb"))
}