From b12d40e8e5c3ba85fc9fda9fce430313a8c23d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 12 Sep 2025 17:13:17 +0200 Subject: [PATCH] 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 --- .../antora-asciidoc-attributes.properties | 1 + documentation/spring-boot-docs/build.gradle | 1 + .../reference/pages/io/spring-batch.adoc | 17 +- .../build.gradle | 45 ++++ ...ongoAutoConfigurationIntegrationTests.java | 98 ++++++++ .../BatchDataMongoAutoConfiguration.java | 149 +++++++++++++ .../BatchDataMongoProperties.java | 96 ++++++++ ...tchMongoSchemaInitializationException.java | 35 +++ .../BatchMongoSchemaInitializer.java | 69 ++++++ ...DependsOnSchemaInitializationDetector.java | 39 ++++ .../mongodb/autoconfigure/package-info.java | 23 ++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../BatchDataMongoAutoConfigurationTests.java | 210 ++++++++++++++++++ .../BatchMongoSchemaInitializerTests.java | 84 +++++++ settings.gradle | 4 + .../build.gradle | 34 +++ .../batch/SampleBatchApplication.java | 46 ++++ .../java/smoketest/batch/package-info.java | 20 ++ .../batch/SampleBatchApplicationTests.java | 53 +++++ .../build.gradle | 26 +++ .../build.gradle | 28 +++ 21 files changed, 1077 insertions(+), 2 deletions(-) create mode 100644 module/spring-boot-batch-data-mongodb/build.gradle create mode 100644 module/spring-boot-batch-data-mongodb/src/dockerTest/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfigurationIntegrationTests.java create mode 100644 module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfiguration.java create mode 100644 module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoProperties.java create mode 100644 module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializationException.java create mode 100644 module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializer.java create mode 100644 module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/JobRepositoryDependsOnSchemaInitializationDetector.java create mode 100644 module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/package-info.java create mode 100644 module/spring-boot-batch-data-mongodb/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 module/spring-boot-batch-data-mongodb/src/test/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfigurationTests.java create mode 100644 module/spring-boot-batch-data-mongodb/src/test/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializerTests.java create mode 100644 smoke-test/spring-boot-smoke-test-batch-data-mongodb/build.gradle create mode 100644 smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/main/java/smoketest/batch/SampleBatchApplication.java create mode 100644 smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/main/java/smoketest/batch/package-info.java create mode 100644 smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/test/java/smoketest/batch/SampleBatchApplicationTests.java create mode 100644 starter/spring-boot-starter-batch-data-mongodb-test/build.gradle create mode 100644 starter/spring-boot-starter-batch-data-mongodb/build.gradle diff --git a/buildSrc/src/main/resources/org/springframework/boot/build/antora/antora-asciidoc-attributes.properties b/buildSrc/src/main/resources/org/springframework/boot/build/antora/antora-asciidoc-attributes.properties index 23061f7cf27..e1fef5f204f 100644 --- a/buildSrc/src/main/resources/org/springframework/boot/build/antora/antora-asciidoc-attributes.properties +++ b/buildSrc/src/main/resources/org/springframework/boot/build/antora/antora-asciidoc-attributes.properties @@ -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 diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index b4a3d44067d..3781363d0bf 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -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")) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/spring-batch.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/spring-batch.adoc index bc6c8ff443f..843fc0642f0 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/spring-batch.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/spring-batch.adoc @@ -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. diff --git a/module/spring-boot-batch-data-mongodb/build.gradle b/module/spring-boot-batch-data-mongodb/build.gradle new file mode 100644 index 00000000000..a49a40e515f --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/build.gradle @@ -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") +} diff --git a/module/spring-boot-batch-data-mongodb/src/dockerTest/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfigurationIntegrationTests.java b/module/spring-boot-batch-data-mongodb/src/dockerTest/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfigurationIntegrationTests.java new file mode 100644 index 00000000000..55baa70cba3 --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/src/dockerTest/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfigurationIntegrationTests.java @@ -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(); + } + + } + +} diff --git a/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfiguration.java b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfiguration.java new file mode 100644 index 00000000000..9638437abb4 --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfiguration.java @@ -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 transactionManager, + @BatchTransactionManager ObjectProvider batchTransactionManager, + @BatchTaskExecutor ObjectProvider 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(); + } + + } + +} diff --git a/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoProperties.java b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoProperties.java new file mode 100644 index 00000000000..47d742b02c2 --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoProperties.java @@ -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; + } + + } + +} diff --git a/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializationException.java b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializationException.java new file mode 100644 index 00000000000..ff8e375d5b9 --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializationException.java @@ -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); + } + +} diff --git a/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializer.java b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializer.java new file mode 100644 index 00000000000..70d2df1ae51 --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializer.java @@ -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); + } + +} diff --git a/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/JobRepositoryDependsOnSchemaInitializationDetector.java b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/JobRepositoryDependsOnSchemaInitializationDetector.java new file mode 100644 index 00000000000..6d9eb02fd1e --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/JobRepositoryDependsOnSchemaInitializationDetector.java @@ -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); + } + +} diff --git a/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/package-info.java b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/package-info.java new file mode 100644 index 00000000000..b4c9a93c093 --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb/autoconfigure/package-info.java @@ -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; diff --git a/module/spring-boot-batch-data-mongodb/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-batch-data-mongodb/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000000..86c6e8af1b3 --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.springframework.boot.batch.mongodb.autoconfigure.BatchDataMongoAutoConfiguration diff --git a/module/spring-boot-batch-data-mongodb/src/test/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfigurationTests.java b/module/spring-boot-batch-data-mongodb/src/test/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfigurationTests.java new file mode 100644 index 00000000000..a16e6ac9003 --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/src/test/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchDataMongoAutoConfigurationTests.java @@ -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); + } + + } + +} diff --git a/module/spring-boot-batch-data-mongodb/src/test/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializerTests.java b/module/spring-boot-batch-data-mongodb/src/test/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializerTests.java new file mode 100644 index 00000000000..1e3fc3f8881 --- /dev/null +++ b/module/spring-boot-batch-data-mongodb/src/test/java/org/springframework/boot/batch/mongodb/autoconfigure/BatchMongoSchemaInitializerTests.java @@ -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))); + } + +} diff --git a/settings.gradle b/settings.gradle index 99feb9ce161..82506ce30b7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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" diff --git a/smoke-test/spring-boot-smoke-test-batch-data-mongodb/build.gradle b/smoke-test/spring-boot-smoke-test-batch-data-mongodb/build.gradle new file mode 100644 index 00000000000..47eaf9992f0 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-batch-data-mongodb/build.gradle @@ -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" +} diff --git a/smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/main/java/smoketest/batch/SampleBatchApplication.java b/smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/main/java/smoketest/batch/SampleBatchApplication.java new file mode 100644 index 00000000000..93a4190821c --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/main/java/smoketest/batch/SampleBatchApplication.java @@ -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))); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/main/java/smoketest/batch/package-info.java b/smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/main/java/smoketest/batch/package-info.java new file mode 100644 index 00000000000..fd87f1b1cbd --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/main/java/smoketest/batch/package-info.java @@ -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; diff --git a/smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/test/java/smoketest/batch/SampleBatchApplicationTests.java b/smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/test/java/smoketest/batch/SampleBatchApplicationTests.java new file mode 100644 index 00000000000..5dd7b1d3bcb --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-batch-data-mongodb/src/test/java/smoketest/batch/SampleBatchApplicationTests.java @@ -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"); + } + +} diff --git a/starter/spring-boot-starter-batch-data-mongodb-test/build.gradle b/starter/spring-boot-starter-batch-data-mongodb-test/build.gradle new file mode 100644 index 00000000000..3d147580a4a --- /dev/null +++ b/starter/spring-boot-starter-batch-data-mongodb-test/build.gradle @@ -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")) +} diff --git a/starter/spring-boot-starter-batch-data-mongodb/build.gradle b/starter/spring-boot-starter-batch-data-mongodb/build.gradle new file mode 100644 index 00000000000..41f6f95c98b --- /dev/null +++ b/starter/spring-boot-starter-batch-data-mongodb/build.gradle @@ -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")) +}