From 4f6bbac13e641dfc4ac7930580313c71ef56847a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 11 Sep 2025 16:59:41 +0200 Subject: [PATCH] Add support for in-memory Batch infrastructure This commit moves the existing JDBC-based Spring Batch infrastructure to a new 'spring-boot-batch-jdbc' module, while the existing module only offers in-memory (aka resourceless) support. The commit also updates the reference guide to provide some more information about what's available and how to use it. Closes gh-46307 --- .../antora/modules/ROOT/pages/redirect.adoc | 4 +- .../antora/modules/how-to/pages/batch.adoc | 14 - .../modules/reference/pages/io/index.adoc | 2 +- .../reference/pages/io/spring-batch.adoc | 47 + .../reference/partials/nav-reference.adoc | 1 + module/spring-boot-batch-jdbc/build.gradle | 49 ++ .../jdbc}/autoconfigure/BatchDataSource.java | 2 +- ...chDataSourceScriptDatabaseInitializer.java | 13 +- .../BatchJdbcAutoConfiguration.java | 192 ++++ .../autoconfigure/BatchJdbcProperties.java | 116 +++ ...pendsOnDatabaseInitializationDetector.java | 2 +- .../jdbc/autoconfigure/package-info.java | 23 + ...itional-spring-configuration-metadata.json | 37 + .../main/resources/META-INF/spring.factories | 2 +- ...ot.autoconfigure.AutoConfiguration.imports | 1 + ...aSourceScriptDatabaseInitializerTests.java | 12 +- .../BatchJdbcAutoConfigurationTests.java | 822 ++++++++++++++++++ ...JdbcAutoConfigurationWithoutJpaTests.java} | 16 +- .../BatchJdbcPropertiesTests.java} | 8 +- .../jdbc}/autoconfigure/domain/City.java | 2 +- .../jdbc}/autoconfigure/custom-schema.sql | 0 module/spring-boot-batch/build.gradle | 8 - .../autoconfigure/BatchAutoConfiguration.java | 152 +--- .../BatchJobLauncherAutoConfiguration.java | 74 ++ .../batch/autoconfigure/BatchProperties.java | 96 -- ...itional-spring-configuration-metadata.json | 33 - ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../BatchAutoConfigurationTests.java | 641 +------------- ...atchJobLauncherAutoConfigurationTests.java | 277 ++++++ .../JobLauncherApplicationRunnerTests.java | 161 +--- settings.gradle | 3 + .../build.gradle | 29 + .../batch/SampleBatchApplication.java | 55 ++ .../java/smoketest/batch/package-info.java | 20 + .../batch/SampleBatchApplicationTests.java | 37 + .../spring-boot-smoke-test-batch/build.gradle | 2 - .../batch/SampleBatchApplication.java | 5 +- .../build.gradle | 28 + .../spring-boot-starter-batch/build.gradle | 2 - 39 files changed, 1900 insertions(+), 1089 deletions(-) create mode 100644 documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/spring-batch.adoc create mode 100644 module/spring-boot-batch-jdbc/build.gradle rename module/{spring-boot-batch/src/main/java/org/springframework/boot/batch => spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc}/autoconfigure/BatchDataSource.java (95%) rename module/{spring-boot-batch/src/main/java/org/springframework/boot/batch => spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc}/autoconfigure/BatchDataSourceScriptDatabaseInitializer.java (89%) create mode 100644 module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcAutoConfiguration.java create mode 100644 module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcProperties.java rename module/{spring-boot-batch/src/main/java/org/springframework/boot/batch => spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc}/autoconfigure/JobRepositoryDependsOnDatabaseInitializationDetector.java (95%) create mode 100644 module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/package-info.java create mode 100644 module/spring-boot-batch-jdbc/src/main/resources/META-INF/additional-spring-configuration-metadata.json rename module/{spring-boot-batch => spring-boot-batch-jdbc}/src/main/resources/META-INF/spring.factories (56%) create mode 100644 module/spring-boot-batch-jdbc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rename module/{spring-boot-batch/src/test/java/org/springframework/boot/batch => spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc}/autoconfigure/BatchDataSourceScriptDatabaseInitializerTests.java (93%) create mode 100644 module/spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcAutoConfigurationTests.java rename module/{spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchAutoConfigurationWithoutJpaTests.java => spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcAutoConfigurationWithoutJpaTests.java} (85%) rename module/{spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchPropertiesTests.java => spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcPropertiesTests.java} (85%) rename module/{spring-boot-batch/src/test/java/org/springframework/boot/batch => spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc}/autoconfigure/domain/City.java (96%) rename module/{spring-boot-batch/src/test/resources/org/springframework/boot/batch => spring-boot-batch-jdbc/src/test/resources/org/springframework/boot/batch/jdbc}/autoconfigure/custom-schema.sql (100%) create mode 100644 module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchJobLauncherAutoConfiguration.java create mode 100644 module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchJobLauncherAutoConfigurationTests.java create mode 100644 smoke-test/spring-boot-smoke-test-batch-jdbc/build.gradle create mode 100644 smoke-test/spring-boot-smoke-test-batch-jdbc/src/main/java/smoketest/batch/SampleBatchApplication.java create mode 100644 smoke-test/spring-boot-smoke-test-batch-jdbc/src/main/java/smoketest/batch/package-info.java create mode 100644 smoke-test/spring-boot-smoke-test-batch-jdbc/src/test/java/smoketest/batch/SampleBatchApplicationTests.java create mode 100644 starter/spring-boot-starter-batch-jdbc/build.gradle diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc index ee6a11ed917..5977f2864ef 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc @@ -387,8 +387,6 @@ * xref:how-to:batch.adoc#howto.batch.restarting-a-failed-job[#howto.batch.restarting-a-failed-job] * xref:how-to:batch.adoc#howto.batch.running-from-the-command-line[#howto-spring-batch-running-command-line] * xref:how-to:batch.adoc#howto.batch.running-from-the-command-line[#howto.batch.running-from-the-command-line] -* xref:how-to:batch.adoc#howto.batch.running-jobs-on-startup[#howto-spring-batch-running-jobs-on-startup] -* xref:how-to:batch.adoc#howto.batch.running-jobs-on-startup[#howto.batch.running-jobs-on-startup] * xref:how-to:batch.adoc#howto.batch.specifying-a-data-source[#howto-spring-batch-specifying-a-data-source] * xref:how-to:batch.adoc#howto.batch.specifying-a-data-source[#howto.batch.specifying-a-data-source] * xref:how-to:batch.adoc#howto.batch.specifying-a-transaction-manager[#howto.batch.specifying-a-transaction-manager] @@ -1633,6 +1631,8 @@ * xref:reference:features/ssl.adoc#features.ssl[#features.ssl] * xref:reference:features/task-execution-and-scheduling.adoc#features.task-execution-and-scheduling[#boot-features-task-execution-scheduling] * xref:reference:features/task-execution-and-scheduling.adoc#features.task-execution-and-scheduling[#features.task-execution-and-scheduling] +* xref:reference:io/spring-batch.adoc#io.spring-batch.running-jobs-on-startup[#howto-spring-batch-running-jobs-on-startup] +* xref:reference:io/spring-batch.adoc#io.spring-batch.running-jobs-on-startup[#howto.batch.running-jobs-on-startup] * xref:reference:io/caching.adoc#io.caching.provider.cache2k[#io.caching.provider.cache2k] * xref:reference:io/caching.adoc#io.caching.provider.caffeine[#boot-features-caching-provider-caffeine] * xref:reference:io/caching.adoc#io.caching.provider.caffeine[#features.caching.provider.caffeine] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/batch.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/batch.adoc index 9c1d3a8783d..b25d9fb5456 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/batch.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/batch.adoc @@ -37,20 +37,6 @@ If you do so and want two task executors (for example by retaining the auto-conf -[[howto.batch.running-jobs-on-startup]] -== Running Spring Batch Jobs on Startup - -Spring Batch auto-configuration is enabled by adding `spring-boot-starter-batch` to your application's classpath. - -If a single javadoc:org.springframework.batch.core.Job[] bean is found in the application context, it is executed on startup (see javadoc:org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner[] for details). -If multiple javadoc:org.springframework.batch.core.Job[] beans are found, the job that should be executed must be specified using configprop:spring.batch.job.name[]. - -To disable running a javadoc:org.springframework.batch.core.Job[] found in the application context, set the configprop:spring.batch.job.enabled[] to `false`. - -See {code-spring-boot-autoconfigure-src}/batch/BatchAutoConfiguration.java[`BatchAutoConfiguration`] for more details. - - - [[howto.batch.running-from-the-command-line]] == Running From the Command Line diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/index.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/index.adoc index 12be6754f78..ccf0ba94899 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/index.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/index.adoc @@ -3,6 +3,6 @@ Most applications will need to deal with input and output concerns at some point. Spring Boot provides utilities and integrations with a range of technologies to help when you need IO capabilities. -This section covers standard IO features such as caching and validation as well as more advanced topics such as scheduling and distributed transactions. +This section covers standard IO features such as caching and validation as well as more advanced topics such as batch, scheduling, and distributed transactions. We will also cover calling remote REST or SOAP services and sending email. 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 new file mode 100644 index 00000000000..da0dcd2900f --- /dev/null +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/spring-batch.adoc @@ -0,0 +1,47 @@ +[[io.spring-batch]] += Spring Batch + +Spring Boot offers several conveniences for working with {url-spring-batch-site}[Spring Batch], including running a Job on startup. + +If Spring Batch is available on your classpath, it is initialized through the javadoc:org.springframework.batch.core.configuration.annotation.EnableBatchProcessing[format=annotation] annotation. + +When building a batch application, the following stores can be auto-configured: + +* In-memory +* JDBC + +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: + +[configprops,yaml] +---- +spring: + batch: + jdbc: + table-prefix: "CUSTOM_" +---- + +You can take control over Spring Batch's configuration using javadoc:org.springframework.batch.core.configuration.annotation.EnableBatchProcessing[format=annotation]. +This will cause the auto-configuration to back off. +Spring Batch can then be configured using the `@Enable*JobRepository` annotation's attributes rather than the previously described configuration properties. + + + +[[io.spring-batch.running-jobs-on-startup]] +== Running Spring Batch Jobs on Startup + +When Spring Boot auto-configures Spring Batch, and if a single javadoc:org.springframework.batch.core.Job[] bean is found in the application context, it is executed on startup (see javadoc:org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner[] for details). +If multiple javadoc:org.springframework.batch.core.Job[] beans are found, the job that should be executed must be specified using configprop:spring.batch.job.name[]. + +You can disable running a javadoc:org.springframework.batch.core.Job[] found in the application context, as shown in the following example: + +[configprops,yaml] +---- +spring: + batch: + job: + enabled: false +---- + + +See javadoc:org.springframework.boot.batch.autoconfigure.BatchAutoConfiguration[] and javadoc:org.springframework.boot.batch.jdbc.autoconfigure.BatchJdbcAutoConfiguration[] for more details. diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/partials/nav-reference.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/partials/nav-reference.adoc index 65cf7db43f9..3d195c46adf 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/partials/nav-reference.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/partials/nav-reference.adoc @@ -39,6 +39,7 @@ ** xref:reference:io/index.adoc[] *** xref:reference:io/caching.adoc[] +*** xref:reference:io/spring-batch.adoc[] *** xref:reference:io/hazelcast.adoc[] *** xref:reference:io/quartz.adoc[] *** xref:reference:io/email.adoc[] diff --git a/module/spring-boot-batch-jdbc/build.gradle b/module/spring-boot-batch-jdbc/build.gradle new file mode 100644 index 00000000000..659ec57a1fe --- /dev/null +++ b/module/spring-boot-batch-jdbc/build.gradle @@ -0,0 +1,49 @@ +/* + * 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.optional-dependencies" +} + +description = "Spring Boot Batch JDBC" + +dependencies { + api(project(":module:spring-boot-batch")) + api(project(":module:spring-boot-jdbc")) + + implementation(project(":module:spring-boot-tx")) + + optional(project(":core:spring-boot-autoconfigure")) + optional(project(":module:spring-boot-hibernate")) + optional(project(":module:spring-boot-micrometer-observation")) + + testImplementation(project(":core:spring-boot-test")) + testImplementation(project(":module:spring-boot-flyway")) + testImplementation(project(":module:spring-boot-liquibase")) + testImplementation(project(":test-support:spring-boot-test-support")) + testImplementation(testFixtures(project(":core:spring-boot-autoconfigure"))) + testImplementation("io.micrometer:micrometer-observation-test") + + testRuntimeOnly("ch.qos.logback:logback-classic") + testRuntimeOnly("com.fasterxml.jackson.core:jackson-databind") + testRuntimeOnly("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + testRuntimeOnly("com.h2database:h2") + testRuntimeOnly("com.zaxxer:HikariCP") +} diff --git a/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchDataSource.java b/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchDataSource.java similarity index 95% rename from module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchDataSource.java rename to module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchDataSource.java index 340902221e7..12e46babab5 100644 --- a/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchDataSource.java +++ b/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchDataSource.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.batch.autoconfigure; +package org.springframework.boot.batch.jdbc.autoconfigure; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchDataSourceScriptDatabaseInitializer.java b/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchDataSourceScriptDatabaseInitializer.java similarity index 89% rename from module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchDataSourceScriptDatabaseInitializer.java rename to module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchDataSourceScriptDatabaseInitializer.java index 47617308736..17389d9459f 100644 --- a/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchDataSourceScriptDatabaseInitializer.java +++ b/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchDataSourceScriptDatabaseInitializer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.batch.autoconfigure; +package org.springframework.boot.batch.jdbc.autoconfigure; import java.util.List; @@ -43,7 +43,7 @@ public class BatchDataSourceScriptDatabaseInitializer extends DataSourceScriptDa * @param properties the Spring Batch JDBC properties * @see #getSettings */ - public BatchDataSourceScriptDatabaseInitializer(DataSource dataSource, BatchProperties.Jdbc properties) { + public BatchDataSourceScriptDatabaseInitializer(DataSource dataSource, BatchJdbcProperties properties) { this(dataSource, getSettings(dataSource, properties)); } @@ -58,16 +58,15 @@ public class BatchDataSourceScriptDatabaseInitializer extends DataSourceScriptDa } /** - * Adapts {@link BatchProperties.Jdbc Spring Batch JDBC properties} to - * {@link DatabaseInitializationSettings} replacing any {@literal @@platform@@} - * placeholders. + * Adapts {@link BatchJdbcProperties} to {@link DatabaseInitializationSettings} + * replacing any {@literal @@platform@@} placeholders. * @param dataSource the Spring Batch data source * @param properties batch JDBC properties * @return a new {@link DatabaseInitializationSettings} instance * @see #BatchDataSourceScriptDatabaseInitializer(DataSource, * DatabaseInitializationSettings) */ - public static DatabaseInitializationSettings getSettings(DataSource dataSource, BatchProperties.Jdbc properties) { + public static DatabaseInitializationSettings getSettings(DataSource dataSource, BatchJdbcProperties properties) { DatabaseInitializationSettings settings = new DatabaseInitializationSettings(); settings.setSchemaLocations(resolveSchemaLocations(dataSource, properties)); settings.setMode(properties.getInitializeSchema()); @@ -75,7 +74,7 @@ public class BatchDataSourceScriptDatabaseInitializer extends DataSourceScriptDa return settings; } - private static List resolveSchemaLocations(DataSource dataSource, BatchProperties.Jdbc properties) { + private static List resolveSchemaLocations(DataSource dataSource, BatchJdbcProperties properties) { PlatformPlaceholderDatabaseDriverResolver platformResolver = new PlatformPlaceholderDatabaseDriverResolver(); if (StringUtils.hasText(properties.getPlatform())) { return platformResolver.resolveAll(properties.getPlatform(), properties.getSchema()); diff --git a/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcAutoConfiguration.java b/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcAutoConfiguration.java new file mode 100644 index 00000000000..65d914eea4e --- /dev/null +++ b/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcAutoConfiguration.java @@ -0,0 +1,192 @@ +/* + * 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.jdbc.autoconfigure; + +import java.util.List; + +import javax.sql.DataSource; + +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.JdbcDefaultBatchConfiguration; +import org.springframework.batch.core.converter.JobParametersConverter; +import org.springframework.batch.core.launch.JobOperator; +import org.springframework.batch.core.repository.ExecutionContextSerializer; +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.batch.autoconfigure.BatchAutoConfiguration; +import org.springframework.boot.batch.autoconfigure.BatchConversionServiceCustomizer; +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.jdbc.autoconfigure.DataSourceAutoConfiguration; +import org.springframework.boot.sql.autoconfigure.init.OnDatabaseInitializationCondition; +import org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer; +import org.springframework.boot.transaction.autoconfigure.TransactionAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.core.task.TaskExecutor; +import org.springframework.jdbc.datasource.init.DatabasePopulator; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Isolation; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Spring Batch using a JDBC store. + * + * @author Dave Syer + * @author EddĂș MelĂ©ndez + * @author Kazuki Shimizu + * @author Mahmoud Ben Hassine + * @author Lars Uffmann + * @author Lasse Wulff + * @author Yanming Zhou + * @since 4.0.0 + */ +@AutoConfiguration(before = { BatchAutoConfiguration.class, BatchJobLauncherAutoConfiguration.class }, + after = { DataSourceAutoConfiguration.class, TransactionAutoConfiguration.class }, + afterName = "org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration") +@ConditionalOnClass({ JobOperator.class, DataSource.class, DatabasePopulator.class }) +@ConditionalOnBean({ DataSource.class, PlatformTransactionManager.class }) +@ConditionalOnMissingBean(value = DefaultBatchConfiguration.class, annotation = EnableBatchProcessing.class) +@EnableConfigurationProperties(BatchJdbcProperties.class) +@Import(DatabaseInitializationDependencyConfigurer.class) +public final class BatchJdbcAutoConfiguration { + + @Configuration(proxyBeanMethods = false) + static class SpringBootBatchJdbcConfiguration extends JdbcDefaultBatchConfiguration { + + private final DataSource dataSource; + + private final PlatformTransactionManager transactionManager; + + private final @Nullable TaskExecutor taskExecutor; + + private final BatchJdbcProperties properties; + + private final List batchConversionServiceCustomizers; + + private final @Nullable ExecutionContextSerializer executionContextSerializer; + + private final @Nullable JobParametersConverter jobParametersConverter; + + SpringBootBatchJdbcConfiguration(DataSource dataSource, + @BatchDataSource ObjectProvider batchDataSource, + PlatformTransactionManager transactionManager, + @BatchTransactionManager ObjectProvider batchTransactionManager, + @BatchTaskExecutor ObjectProvider batchTaskExecutor, BatchJdbcProperties properties, + ObjectProvider batchConversionServiceCustomizers, + ObjectProvider executionContextSerializer, + ObjectProvider jobParametersConverter) { + this.dataSource = batchDataSource.getIfAvailable(() -> dataSource); + this.transactionManager = batchTransactionManager.getIfAvailable(() -> transactionManager); + this.taskExecutor = batchTaskExecutor.getIfAvailable(); + this.properties = properties; + this.batchConversionServiceCustomizers = batchConversionServiceCustomizers.orderedStream().toList(); + this.executionContextSerializer = executionContextSerializer.getIfAvailable(); + this.jobParametersConverter = jobParametersConverter.getIfAvailable(); + } + + @Override + protected DataSource getDataSource() { + return this.dataSource; + } + + @Override + protected PlatformTransactionManager getTransactionManager() { + return this.transactionManager; + } + + @Override + protected String getTablePrefix() { + String tablePrefix = this.properties.getTablePrefix(); + return (tablePrefix != null) ? tablePrefix : super.getTablePrefix(); + } + + @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 ConfigurableConversionService getConversionService() { + ConfigurableConversionService conversionService = super.getConversionService(); + for (BatchConversionServiceCustomizer customizer : this.batchConversionServiceCustomizers) { + customizer.customize(conversionService); + } + return conversionService; + } + + @Override + protected ExecutionContextSerializer getExecutionContextSerializer() { + return (this.executionContextSerializer != null) ? this.executionContextSerializer + : super.getExecutionContextSerializer(); + } + + @Override + @Deprecated(since = "4.0.0", forRemoval = true) + @SuppressWarnings("removal") + protected JobParametersConverter getJobParametersConverter() { + return (this.jobParametersConverter != null) ? this.jobParametersConverter + : super.getJobParametersConverter(); + } + + @Override + protected TaskExecutor getTaskExecutor() { + return (this.taskExecutor != null) ? this.taskExecutor : super.getTaskExecutor(); + } + + @Configuration(proxyBeanMethods = false) + @Conditional(OnBatchDatasourceInitializationCondition.class) + static class DataSourceInitializerConfiguration { + + @Bean + @ConditionalOnMissingBean + BatchDataSourceScriptDatabaseInitializer batchDataSourceInitializer(DataSource dataSource, + @BatchDataSource ObjectProvider batchDataSource, BatchJdbcProperties properties) { + return new BatchDataSourceScriptDatabaseInitializer(batchDataSource.getIfAvailable(() -> dataSource), + properties); + } + + } + + static class OnBatchDatasourceInitializationCondition extends OnDatabaseInitializationCondition { + + OnBatchDatasourceInitializationCondition() { + super("Batch", "spring.batch.jdbc.initialize-schema"); + } + + } + + } + +} diff --git a/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcProperties.java b/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcProperties.java new file mode 100644 index 00000000000..7a808ba44d2 --- /dev/null +++ b/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcProperties.java @@ -0,0 +1,116 @@ +/* + * 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.jdbc.autoconfigure; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.sql.init.DatabaseInitializationMode; +import org.springframework.transaction.annotation.Isolation; + +/** + * Configuration properties for Spring Batch using a JDBC store. + * + * @author Stephane Nicoll + * @since 4.0.0 + */ +@ConfigurationProperties("spring.batch.jdbc") +public class BatchJdbcProperties { + + private static final String DEFAULT_SCHEMA_LOCATION = "classpath:org/springframework/" + + "batch/core/schema-@@platform@@.sql"; + + /** + * Whether to validate the transaction state. + */ + private boolean validateTransactionState = true; + + /** + * Transaction isolation level to use when creating job meta-data for new jobs. + */ + private @Nullable Isolation isolationLevelForCreate; + + /** + * Path to the SQL file to use to initialize the database schema. + */ + private String schema = DEFAULT_SCHEMA_LOCATION; + + /** + * Platform to use in initialization scripts if the @@platform@@ placeholder is used. + * Auto-detected by default. + */ + private @Nullable String platform; + + /** + * Table prefix for all the batch meta-data tables. + */ + private @Nullable String tablePrefix; + + /** + * Database schema initialization mode. + */ + private DatabaseInitializationMode initializeSchema = DatabaseInitializationMode.EMBEDDED; + + 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 String getSchema() { + return this.schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + + public @Nullable String getPlatform() { + return this.platform; + } + + public void setPlatform(@Nullable String platform) { + this.platform = platform; + } + + public @Nullable String getTablePrefix() { + return this.tablePrefix; + } + + public void setTablePrefix(@Nullable String tablePrefix) { + this.tablePrefix = tablePrefix; + } + + public DatabaseInitializationMode getInitializeSchema() { + return this.initializeSchema; + } + + public void setInitializeSchema(DatabaseInitializationMode initializeSchema) { + this.initializeSchema = initializeSchema; + } + +} diff --git a/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/JobRepositoryDependsOnDatabaseInitializationDetector.java b/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/JobRepositoryDependsOnDatabaseInitializationDetector.java similarity index 95% rename from module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/JobRepositoryDependsOnDatabaseInitializationDetector.java rename to module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/JobRepositoryDependsOnDatabaseInitializationDetector.java index 0ccfbca7c31..6818866126e 100644 --- a/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/JobRepositoryDependsOnDatabaseInitializationDetector.java +++ b/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/JobRepositoryDependsOnDatabaseInitializationDetector.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.batch.autoconfigure; +package org.springframework.boot.batch.jdbc.autoconfigure; import java.util.Collections; import java.util.Set; diff --git a/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/package-info.java b/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/autoconfigure/package-info.java new file mode 100644 index 00000000000..e246e092579 --- /dev/null +++ b/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc/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 JDBC. + */ +@NullMarked +package org.springframework.boot.batch.jdbc.autoconfigure; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-batch-jdbc/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-batch-jdbc/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 00000000000..1747492d8c0 --- /dev/null +++ b/module/spring-boot-batch-jdbc/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,37 @@ +{ + "properties": [ + { + "name": "spring.batch.initialize-schema", + "type": "org.springframework.boot.sql.init.DatabaseInitializationMode", + "deprecation": { + "replacement": "spring.batch.jdbc.initialize-schema", + "level": "error" + } + }, + { + "name": "spring.batch.initializer.enabled", + "type": "java.lang.Boolean", + "description": "Create the required batch tables on startup if necessary. Enabled automatically\n if no custom table prefix is set or if a custom schema is configured.", + "deprecation": { + "replacement": "spring.batch.jdbc.initialize-schema", + "level": "error" + } + }, + { + "name": "spring.batch.schema", + "type": "java.lang.String", + "deprecation": { + "replacement": "spring.batch.jdbc.schema", + "level": "error" + } + }, + { + "name": "spring.batch.table-prefix", + "type": "java.lang.String", + "deprecation": { + "replacement": "spring.batch.jdbc.table-prefix", + "level": "error" + } + } + ] +} \ No newline at end of file diff --git a/module/spring-boot-batch/src/main/resources/META-INF/spring.factories b/module/spring-boot-batch-jdbc/src/main/resources/META-INF/spring.factories similarity index 56% rename from module/spring-boot-batch/src/main/resources/META-INF/spring.factories rename to module/spring-boot-batch-jdbc/src/main/resources/META-INF/spring.factories index f1572ab5731..714b3b9b81a 100644 --- a/module/spring-boot-batch/src/main/resources/META-INF/spring.factories +++ b/module/spring-boot-batch-jdbc/src/main/resources/META-INF/spring.factories @@ -1,3 +1,3 @@ # Depends on Database Initialization Detectors org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector=\ -org.springframework.boot.batch.autoconfigure.JobRepositoryDependsOnDatabaseInitializationDetector +org.springframework.boot.batch.jdbc.autoconfigure.JobRepositoryDependsOnDatabaseInitializationDetector diff --git a/module/spring-boot-batch-jdbc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-batch-jdbc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000000..79a81b61c34 --- /dev/null +++ b/module/spring-boot-batch-jdbc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.springframework.boot.batch.jdbc.autoconfigure.BatchJdbcAutoConfiguration diff --git a/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchDataSourceScriptDatabaseInitializerTests.java b/module/spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchDataSourceScriptDatabaseInitializerTests.java similarity index 93% rename from module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchDataSourceScriptDatabaseInitializerTests.java rename to module/spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchDataSourceScriptDatabaseInitializerTests.java index 6d9ddbdad01..7cc1ec63716 100644 --- a/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchDataSourceScriptDatabaseInitializerTests.java +++ b/module/spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchDataSourceScriptDatabaseInitializerTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.batch.autoconfigure; +package org.springframework.boot.batch.jdbc.autoconfigure; import java.io.IOException; import java.sql.Connection; @@ -52,10 +52,10 @@ class BatchDataSourceScriptDatabaseInitializerTests { @Test void getSettingsWithPlatformDoesNotTouchDataSource() { DataSource dataSource = mock(DataSource.class); - BatchProperties properties = new BatchProperties(); - properties.getJdbc().setPlatform("test"); + BatchJdbcProperties properties = new BatchJdbcProperties(); + properties.setPlatform("test"); DatabaseInitializationSettings settings = BatchDataSourceScriptDatabaseInitializer.getSettings(dataSource, - properties.getJdbc()); + properties); assertThat(settings.getSchemaLocations()) .containsOnly("classpath:org/springframework/batch/core/schema-test.sql"); then(dataSource).shouldHaveNoInteractions(); @@ -66,7 +66,7 @@ class BatchDataSourceScriptDatabaseInitializerTests { "INFORMIX", "JTDS", "PHOENIX", "REDSHIFT", "TERADATA", "TESTCONTAINERS", "UNKNOWN" }) void batchSchemaCanBeLocated(DatabaseDriver driver) throws SQLException { DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); - BatchProperties properties = new BatchProperties(); + BatchJdbcProperties properties = new BatchJdbcProperties(); DataSource dataSource = mock(DataSource.class); Connection connection = mock(Connection.class); given(dataSource.getConnection()).willReturn(connection); @@ -75,7 +75,7 @@ class BatchDataSourceScriptDatabaseInitializerTests { String productName = (String) ReflectionTestUtils.getField(driver, "productName"); given(metadata.getDatabaseProductName()).willReturn(productName); DatabaseInitializationSettings settings = BatchDataSourceScriptDatabaseInitializer.getSettings(dataSource, - properties.getJdbc()); + properties); List schemaLocations = settings.getSchemaLocations(); assertThat(schemaLocations).isNotEmpty() .allSatisfy((location) -> assertThat(resourceLoader.getResource(location).exists()).isTrue()); diff --git a/module/spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcAutoConfigurationTests.java b/module/spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcAutoConfigurationTests.java new file mode 100644 index 00000000000..468ed9de91d --- /dev/null +++ b/module/spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcAutoConfigurationTests.java @@ -0,0 +1,822 @@ +/* + * 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.jdbc.autoconfigure; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import javax.sql.DataSource; + +import jakarta.persistence.EntityManagerFactory; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; +import org.mockito.Mockito; + +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.configuration.JobRegistry; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration; +import org.springframework.batch.core.converter.DefaultJobParametersConverter; +import org.springframework.batch.core.converter.JobParametersConverter; +import org.springframework.batch.core.converter.JsonJobParametersConverter; +import org.springframework.batch.core.job.AbstractJob; +import org.springframework.batch.core.job.Job; +import org.springframework.batch.core.job.JobExecution; +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.ExecutionContextSerializer; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.repository.dao.DefaultExecutionContextSerializer; +import org.springframework.batch.core.repository.dao.Jackson2ExecutionContextStringSerializer; +import org.springframework.batch.core.step.Step; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.DefaultApplicationArguments; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; +import org.springframework.boot.batch.autoconfigure.BatchConversionServiceCustomizer; +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.batch.autoconfigure.JobLauncherApplicationRunner; +import org.springframework.boot.batch.jdbc.autoconfigure.BatchJdbcAutoConfiguration.SpringBootBatchJdbcConfiguration; +import org.springframework.boot.batch.jdbc.autoconfigure.domain.City; +import org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration; +import org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration; +import org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.jdbc.autoconfigure.EmbeddedDataSourceConfiguration; +import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; +import org.springframework.boot.liquibase.autoconfigure.LiquibaseAutoConfiguration; +import org.springframework.boot.sql.init.DatabaseInitializationMode; +import org.springframework.boot.sql.init.DatabaseInitializationSettings; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.resources.WithPackageResources; +import org.springframework.boot.testsupport.classpath.resources.WithResource; +import org.springframework.boot.transaction.autoconfigure.TransactionAutoConfiguration; +import org.springframework.boot.transaction.autoconfigure.TransactionManagerCustomizationAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.annotation.Order; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.SyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.init.DatabasePopulator; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.test.util.AopTestUtils; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Isolation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BatchJdbcAutoConfiguration}. + * + * @author Dave Syer + * @author Stephane Nicoll + * @author Vedran Pavic + * @author Kazuki Shimizu + * @author Mahmoud Ben Hassine + * @author Lars Uffmann + * @author Lasse Wulff + * @author Yanming Zhou + */ +class BatchJdbcAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(BatchJobLauncherAutoConfiguration.class, + BatchJdbcAutoConfiguration.class, TransactionManagerCustomizationAutoConfiguration.class, + TransactionAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class)); + + @Test + void testDefaultContext() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(JobRepository.class); + assertThat(context).hasSingleBean(JobOperator.class); + assertThat(context.getBean(BatchJdbcProperties.class).getInitializeSchema()) + .isEqualTo(DatabaseInitializationMode.EMBEDDED); + assertThat(new JdbcTemplate(context.getBean(DataSource.class)) + .queryForList("select * from BATCH_JOB_EXECUTION")).isEmpty(); + }); + } + + @Test + void autoconfigurationBacksOffEntirelyIfSpringJdbcAbsent() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withClassLoader(new FilteredClassLoader(DatabasePopulator.class)) + .run((context) -> { + assertThat(context).doesNotHaveBean(JobLauncherApplicationRunner.class); + assertThat(context).doesNotHaveBean(BatchDataSourceScriptDatabaseInitializer.class); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserEnablesBatchProcessing() { + this.contextRunner + .withUserConfiguration(EnableBatchProcessingConfiguration.class, EmbeddedDataSourceConfiguration.class) + .withClassLoader(new FilteredClassLoader(DatabasePopulator.class)) + .run((context) -> assertThat(context).doesNotHaveBean(BatchJdbcAutoConfiguration.class)); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesBatchConfiguration() { + this.contextRunner.withUserConfiguration(CustomBatchConfiguration.class, EmbeddedDataSourceConfiguration.class) + .withClassLoader(new FilteredClassLoader(DatabasePopulator.class)) + .run((context) -> assertThat(context).doesNotHaveBean(BatchJdbcAutoConfiguration.class)); + } + + @Test + void testDefinesAndLaunchesJob() { + this.contextRunner.withUserConfiguration(JobConfiguration.class, EmbeddedDataSourceConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(JobOperator.class); + context.getBean(JobLauncherApplicationRunner.class) + .run(new DefaultApplicationArguments("jobParam=test")); + JobParameters jobParameters = new JobParametersBuilder().addString("jobParam", "test") + .toJobParameters(); + assertThat(context.getBean(JobRepository.class).getLastJobExecution("job", jobParameters)).isNotNull(); + }); + } + + @Test + void testDefinesAndLaunchesJobIgnoreOptionArguments() { + this.contextRunner.withUserConfiguration(JobConfiguration.class, EmbeddedDataSourceConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(JobOperator.class); + context.getBean(JobLauncherApplicationRunner.class) + .run(new DefaultApplicationArguments("--spring.property=value", "jobParam=test")); + JobParameters jobParameters = new JobParametersBuilder().addString("jobParam", "test") + .toJobParameters(); + assertThat(context.getBean(JobRepository.class).getLastJobExecution("job", jobParameters)).isNotNull(); + }); + } + + @Test + void testRegisteredAndLocalJob() { + this.contextRunner + .withUserConfiguration(NamedJobConfigurationWithRegisteredAndLocalJob.class, + EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.batch.job.name:discreteRegisteredJob") + .run((context) -> { + assertThat(context).hasSingleBean(JobOperator.class); + context.getBean(JobLauncherApplicationRunner.class).run(); + assertThat(context.getBean(JobRepository.class) + .getLastJobExecution("discreteRegisteredJob", new JobParameters()) + .getStatus()).isEqualTo(BatchStatus.COMPLETED); + }); + } + + @Test + void testDefinesAndLaunchesLocalJob() { + this.contextRunner + .withUserConfiguration(NamedJobConfigurationWithLocalJob.class, EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.batch.job.name:discreteLocalJob") + .run((context) -> { + assertThat(context).hasSingleBean(JobOperator.class); + context.getBean(JobLauncherApplicationRunner.class).run(); + assertThat(context.getBean(JobRepository.class) + .getLastJobExecution("discreteLocalJob", new JobParameters())).isNotNull(); + }); + } + + @Test + void testMultipleJobsAndNoJobName() { + this.contextRunner.withUserConfiguration(MultipleJobConfiguration.class, EmbeddedDataSourceConfiguration.class) + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure().getCause().getMessage()) + .contains("Job name must be specified in case of multiple jobs"); + }); + } + + @Test + void testMultipleJobsAndJobName() { + this.contextRunner.withUserConfiguration(MultipleJobConfiguration.class, EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.batch.job.name:discreteLocalJob") + .run((context) -> { + assertThat(context).hasSingleBean(JobOperator.class); + context.getBean(JobLauncherApplicationRunner.class).run(); + assertThat(context.getBean(JobRepository.class) + .getLastJobExecution("discreteLocalJob", new JobParameters())).isNotNull(); + }); + } + + @Test + void testDisableLaunchesJob() { + this.contextRunner.withUserConfiguration(JobConfiguration.class, EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.batch.job.enabled:false") + .run((context) -> { + assertThat(context).hasSingleBean(JobOperator.class); + assertThat(context).doesNotHaveBean(CommandLineRunner.class); + }); + } + + @Test + void testDisableSchemaLoader() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.datasource.generate-unique-name=true", + "spring.batch.jdbc.initialize-schema:never") + .run((context) -> { + assertThat(context).hasSingleBean(JobOperator.class); + assertThat(context.getBean(BatchJdbcProperties.class).getInitializeSchema()) + .isEqualTo(DatabaseInitializationMode.NEVER); + assertThat(context).doesNotHaveBean(BatchDataSourceScriptDatabaseInitializer.class); + assertThatExceptionOfType(BadSqlGrammarException.class) + .isThrownBy(() -> new JdbcTemplate(context.getBean(DataSource.class)) + .queryForList("select * from BATCH_JOB_EXECUTION")); + }); + } + + @Test + void testUsingJpa() { + this.contextRunner + .withUserConfiguration(TestJpaConfiguration.class, EmbeddedDataSourceConfiguration.class, + HibernateJpaAutoConfiguration.class) + .run((context) -> { + PlatformTransactionManager transactionManager = context.getBean(PlatformTransactionManager.class); + // It's a lazy proxy, but it does render its target if you ask for + // toString(): + assertThat(transactionManager.toString()).contains("JpaTransactionManager"); + assertThat(context).hasSingleBean(EntityManagerFactory.class); + // Ensure the JobRepository can be used (no problem with isolation + // level) + assertThat(context.getBean(JobRepository.class).getLastJobExecution("job", new JobParameters())) + .isNull(); + }); + } + + @Test + @WithPackageResources("custom-schema.sql") + void testRenamePrefix() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.datasource.generate-unique-name=true", + "spring.batch.jdbc.schema:classpath:custom-schema.sql", "spring.batch.jdbc.table-prefix:PREFIX_") + .run((context) -> { + assertThat(context).hasSingleBean(JobOperator.class); + assertThat(context.getBean(BatchJdbcProperties.class).getInitializeSchema()) + .isEqualTo(DatabaseInitializationMode.EMBEDDED); + assertThat(new JdbcTemplate(context.getBean(DataSource.class)) + .queryForList("select * from PREFIX_JOB_EXECUTION")).isEmpty(); + JobRepository jobRepository = context.getBean(JobRepository.class); + assertThat(jobRepository.findRunningJobExecutions("test")).isEmpty(); + assertThat(jobRepository.getLastJobExecution("test", new JobParameters())).isNull(); + }); + } + + @Test + void testCustomizeJpaTransactionManagerUsingProperties() { + this.contextRunner + .withUserConfiguration(TestJpaConfiguration.class, EmbeddedDataSourceConfiguration.class, + HibernateJpaAutoConfiguration.class) + .withPropertyValues("spring.transaction.default-timeout:30", + "spring.transaction.rollback-on-commit-failure:true") + .run((context) -> { + assertThat(context).hasSingleBean(BatchJobLauncherAutoConfiguration.class); + JpaTransactionManager transactionManager = JpaTransactionManager.class + .cast(context.getBean(SpringBootBatchJdbcConfiguration.class).getTransactionManager()); + assertThat(transactionManager.getDefaultTimeout()).isEqualTo(30); + assertThat(transactionManager.isRollbackOnCommitFailure()).isTrue(); + }); + } + + @Test + void testCustomizeDataSourceTransactionManagerUsingProperties() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.transaction.default-timeout:30", + "spring.transaction.rollback-on-commit-failure:true") + .run((context) -> { + assertThat(context).hasSingleBean(BatchJdbcAutoConfiguration.class); + DataSourceTransactionManager transactionManager = DataSourceTransactionManager.class + .cast(context.getBean(SpringBootBatchJdbcConfiguration.class).getTransactionManager()); + assertThat(transactionManager.getDefaultTimeout()).isEqualTo(30); + assertThat(transactionManager.isRollbackOnCommitFailure()).isTrue(); + }); + } + + @Test + void testBatchDataSource() { + this.contextRunner.withUserConfiguration(BatchDataSourceConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(BatchJdbcAutoConfiguration.class) + .hasSingleBean(BatchDataSourceScriptDatabaseInitializer.class) + .hasBean("batchDataSource"); + DataSource batchDataSource = context.getBean("batchDataSource", DataSource.class); + assertThat(context.getBean(SpringBootBatchJdbcConfiguration.class).getDataSource()) + .isEqualTo(batchDataSource); + assertThat(context.getBean(BatchDataSourceScriptDatabaseInitializer.class)) + .hasFieldOrPropertyWithValue("dataSource", batchDataSource); + }); + } + + @Test + void testBatchTransactionManager() { + this.contextRunner.withUserConfiguration(BatchTransactionManagerConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(BatchJdbcAutoConfiguration.class); + PlatformTransactionManager batchTransactionManager = context.getBean("batchTransactionManager", + PlatformTransactionManager.class); + assertThat(context.getBean(SpringBootBatchJdbcConfiguration.class).getTransactionManager()) + .isEqualTo(batchTransactionManager); + }); + } + + @Test + void testBatchTaskExecutor() { + this.contextRunner + .withUserConfiguration(BatchTaskExecutorConfiguration.class, EmbeddedDataSourceConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(BatchJdbcAutoConfiguration.class).hasBean("batchTaskExecutor"); + TaskExecutor batchTaskExecutor = context.getBean("batchTaskExecutor", TaskExecutor.class); + assertThat(batchTaskExecutor).isInstanceOf(AsyncTaskExecutor.class); + assertThat(context.getBean(SpringBootBatchJdbcConfiguration.class).getTaskExecutor()) + .isEqualTo(batchTaskExecutor); + JobOperator jobOperator = AopTestUtils.getTargetObject(context.getBean(JobOperator.class)); + assertThat(jobOperator).hasFieldOrPropertyWithValue("taskExecutor", batchTaskExecutor); + }); + } + + @Test + void jobRepositoryBeansDependOnBatchDataSourceInitializer() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class).run((context) -> { + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + String[] jobRepositoryNames = beanFactory.getBeanNamesForType(JobRepository.class); + assertThat(jobRepositoryNames).isNotEmpty(); + for (String jobRepositoryName : jobRepositoryNames) { + assertThat(beanFactory.getBeanDefinition(jobRepositoryName).getDependsOn()) + .contains("batchDataSourceInitializer"); + } + }); + } + + @Test + void jobRepositoryBeansDependOnFlyway() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class, FlywayAutoConfiguration.class) + .withPropertyValues("spring.batch.jdbc.initialize-schema=never") + .run((context) -> { + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + String[] jobRepositoryNames = beanFactory.getBeanNamesForType(JobRepository.class); + assertThat(jobRepositoryNames).isNotEmpty(); + for (String jobRepositoryName : jobRepositoryNames) { + assertThat(beanFactory.getBeanDefinition(jobRepositoryName).getDependsOn()).contains("flyway", + "flywayInitializer"); + } + }); + } + + @Test + @WithResource(name = "db/changelog/db.changelog-master.yaml", content = "databaseChangeLog:") + void jobRepositoryBeansDependOnLiquibase() { + this.contextRunner + .withUserConfiguration(EmbeddedDataSourceConfiguration.class, LiquibaseAutoConfiguration.class) + .withPropertyValues("spring.batch.jdbc.initialize-schema=never") + .run((context) -> { + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + String[] jobRepositoryNames = beanFactory.getBeanNamesForType(JobRepository.class); + assertThat(jobRepositoryNames).isNotEmpty(); + for (String jobRepositoryName : jobRepositoryNames) { + assertThat(beanFactory.getBeanDefinition(jobRepositoryName).getDependsOn()).contains("liquibase"); + } + }); + } + + @Test + void whenTheUserDefinesTheirOwnBatchDatabaseInitializerThenTheAutoConfiguredInitializerBacksOff() { + this.contextRunner.withUserConfiguration(CustomBatchDatabaseInitializerConfiguration.class) + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class)) + .run((context) -> assertThat(context).hasSingleBean(BatchDataSourceScriptDatabaseInitializer.class) + .doesNotHaveBean("batchDataSourceScriptDatabaseInitializer") + .hasBean("customInitializer")); + } + + @Test + void whenTheUserDefinesTheirOwnDatabaseInitializerThenTheAutoConfiguredBatchInitializerRemains() { + this.contextRunner.withUserConfiguration(CustomDatabaseInitializerConfiguration.class) + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class)) + .run((context) -> assertThat(context).hasSingleBean(BatchDataSourceScriptDatabaseInitializer.class) + .hasBean("customInitializer")); + } + + @Test + void conversionServiceCustomizersAreCalled() { + this.contextRunner + .withUserConfiguration(EmbeddedDataSourceConfiguration.class, + ConversionServiceCustomizersConfiguration.class) + .run((context) -> { + BatchConversionServiceCustomizer customizer = context.getBean("batchConversionServiceCustomizer", + BatchConversionServiceCustomizer.class); + BatchConversionServiceCustomizer anotherCustomizer = context + .getBean("anotherBatchConversionServiceCustomizer", BatchConversionServiceCustomizer.class); + InOrder inOrder = Mockito.inOrder(customizer, anotherCustomizer); + ConfigurableConversionService configurableConversionService = context + .getBean(SpringBootBatchJdbcConfiguration.class) + .getConversionService(); + inOrder.verify(customizer).customize(configurableConversionService); + inOrder.verify(anotherCustomizer).customize(configurableConversionService); + }); + } + + @Test + void whenTheUserDefinesAJobNameAsJobInstanceValidates() { + JobLauncherApplicationRunner runner = createInstance("another"); + runner.setJobs(Collections.singletonList(mockJob("test"))); + runner.setJobName("test"); + runner.afterPropertiesSet(); + } + + @Test + void whenTheUserDefinesAJobNameAsRegisteredJobValidates() { + JobLauncherApplicationRunner runner = createInstance("test"); + runner.setJobName("test"); + runner.afterPropertiesSet(); + } + + @Test + void whenTheUserDefinesAJobNameThatDoesNotExistWithJobInstancesFailsFast() { + JobLauncherApplicationRunner runner = createInstance(); + runner.setJobs(Arrays.asList(mockJob("one"), mockJob("two"))); + runner.setJobName("three"); + assertThatIllegalStateException().isThrownBy(runner::afterPropertiesSet) + .withMessage("No job found with name 'three'"); + } + + @Test + void whenTheUserDefinesAJobNameThatDoesNotExistWithRegisteredJobFailsFast() { + JobLauncherApplicationRunner runner = createInstance("one", "two"); + runner.setJobName("three"); + assertThatIllegalStateException().isThrownBy(runner::afterPropertiesSet) + .withMessage("No job found with name 'three'"); + } + + @Test + void customExecutionContextSerializerIsUsed() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withBean(ExecutionContextSerializer.class, Jackson2ExecutionContextStringSerializer::new) + .run((context) -> { + assertThat(context).hasSingleBean(Jackson2ExecutionContextStringSerializer.class); + assertThat(context.getBean(SpringBootBatchJdbcConfiguration.class).getExecutionContextSerializer()) + .isInstanceOf(Jackson2ExecutionContextStringSerializer.class); + }); + } + + @Test + void defaultExecutionContextSerializerIsUsed() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class).run((context) -> { + assertThat(context).doesNotHaveBean(ExecutionContextSerializer.class); + assertThat(context.getBean(SpringBootBatchJdbcConfiguration.class).getExecutionContextSerializer()) + .isInstanceOf(DefaultExecutionContextSerializer.class); + }); + } + + @Test + void customJdbcPropertiesIsUsed() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.batch.jdbc.validate-transaction-state:false", + "spring.batch.jdbc.isolation-level-for-create:READ_COMMITTED") + .run((context) -> { + SpringBootBatchJdbcConfiguration configuration = context + .getBean(SpringBootBatchJdbcConfiguration.class); + assertThat(configuration.getValidateTransactionState()).isEqualTo(false); + assertThat(configuration.getIsolationLevelForCreate()).isEqualTo(Isolation.READ_COMMITTED); + }); + + } + + @Test + @Deprecated(since = "4.0.0", forRemoval = true) + @SuppressWarnings("removal") + void customJobParametersConverterIsUsed() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withBean(JobParametersConverter.class, JsonJobParametersConverter::new) + .withPropertyValues("spring.datasource.generate-unique-name=true") + .run((context) -> { + assertThat(context).hasSingleBean(JsonJobParametersConverter.class); + assertThat(context.getBean(SpringBootBatchJdbcConfiguration.class).getJobParametersConverter()) + .isInstanceOf(JsonJobParametersConverter.class); + }); + } + + @Test + @Deprecated(since = "4.0.0", forRemoval = true) + @SuppressWarnings("removal") + void defaultJobParametersConverterIsUsed() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class).run((context) -> { + assertThat(context).doesNotHaveBean(JobParametersConverter.class); + assertThat(context.getBean(SpringBootBatchJdbcConfiguration.class).getJobParametersConverter()) + .isInstanceOf(DefaultJobParametersConverter.class); + }); + } + + private JobLauncherApplicationRunner createInstance(String... registeredJobNames) { + JobLauncherApplicationRunner runner = new JobLauncherApplicationRunner(mock(JobOperator.class)); + JobRegistry jobRegistry = mock(JobRegistry.class); + given(jobRegistry.getJobNames()).willReturn(Arrays.asList(registeredJobNames)); + runner.setJobRegistry(jobRegistry); + return runner; + } + + private Job mockJob(String name) { + Job job = mock(Job.class); + given(job.getName()).willReturn(name); + return job; + } + + @Configuration(proxyBeanMethods = false) + static class BatchDataSourceConfiguration { + + @Bean + DataSource normalDataSource() { + return DataSourceBuilder.create().url("jdbc:h2:mem:normal").username("sa").build(); + } + + @BatchDataSource + @Bean(defaultCandidate = false) + DataSource batchDataSource() { + return DataSourceBuilder.create().url("jdbc:h2:mem:batchdatasource").username("sa").build(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class BatchTransactionManagerConfiguration { + + @Bean + DataSource dataSource() { + return DataSourceBuilder.create().url("jdbc:h2:mem:database").username("sa").build(); + } + + @Bean + @Primary + PlatformTransactionManager normalTransactionManager() { + return mock(PlatformTransactionManager.class); + } + + @BatchTransactionManager + @Bean(defaultCandidate = false) + PlatformTransactionManager batchTransactionManager() { + return mock(PlatformTransactionManager.class); + } + + } + + @Configuration(proxyBeanMethods = false) + static class BatchTaskExecutorConfiguration { + + @Bean + TaskExecutor taskExecutor() { + return new SyncTaskExecutor(); + } + + @BatchTaskExecutor + @Bean(defaultCandidate = false) + TaskExecutor batchTaskExecutor() { + return new SimpleAsyncTaskExecutor(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class EmptyConfiguration { + + } + + @TestAutoConfigurationPackage(City.class) + static class TestJpaConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + static class EntityManagerFactoryConfiguration { + + @Bean + EntityManagerFactory entityManagerFactory() { + return mock(EntityManagerFactory.class); + } + + } + + @Configuration(proxyBeanMethods = false) + static class NamedJobConfigurationWithRegisteredAndLocalJob { + + @Autowired + private JobRepository jobRepository; + + @Bean + Job discreteJob() { + AbstractJob job = new AbstractJob("discreteRegisteredJob") { + + private static int count = 0; + + @Override + public Collection getStepNames() { + return Collections.emptySet(); + } + + @Override + public Step getStep(String stepName) { + return null; + } + + @Override + protected void doExecute(JobExecution execution) { + if (count == 0) { + execution.setStatus(BatchStatus.COMPLETED); + } + else { + execution.setStatus(BatchStatus.FAILED); + } + count++; + } + }; + job.setJobRepository(this.jobRepository); + return job; + } + + } + + @Configuration(proxyBeanMethods = false) + static class NamedJobConfigurationWithLocalJob { + + @Autowired + private JobRepository jobRepository; + + @Bean + Job discreteJob() { + AbstractJob job = new AbstractJob("discreteLocalJob") { + + @Override + public Collection getStepNames() { + return Collections.emptySet(); + } + + @Override + public Step getStep(String stepName) { + return null; + } + + @Override + protected void doExecute(JobExecution execution) { + execution.setStatus(BatchStatus.COMPLETED); + } + }; + job.setJobRepository(this.jobRepository); + return job; + } + + } + + @Configuration(proxyBeanMethods = false) + static class MultipleJobConfiguration { + + @Autowired + private JobRepository jobRepository; + + @Bean + Job discreteJob() { + AbstractJob job = new AbstractJob("discreteLocalJob") { + + @Override + public Collection getStepNames() { + return Collections.emptySet(); + } + + @Override + public Step getStep(String stepName) { + return null; + } + + @Override + protected void doExecute(JobExecution execution) { + execution.setStatus(BatchStatus.COMPLETED); + } + }; + job.setJobRepository(this.jobRepository); + return job; + } + + @Bean + Job job2() { + return new Job() { + @Override + public String getName() { + return "discreteLocalJob2"; + } + + @Override + public void execute(JobExecution execution) { + execution.setStatus(BatchStatus.COMPLETED); + } + }; + } + + } + + @Configuration(proxyBeanMethods = false) + static class JobConfiguration { + + @Autowired + private JobRepository jobRepository; + + @Bean + Job job() { + AbstractJob job = new AbstractJob() { + + @Override + public Collection getStepNames() { + return Collections.emptySet(); + } + + @Override + public Step getStep(String stepName) { + return null; + } + + @Override + protected void doExecute(JobExecution execution) { + execution.setStatus(BatchStatus.COMPLETED); + } + }; + job.setJobRepository(this.jobRepository); + return job; + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomBatchDatabaseInitializerConfiguration { + + @Bean + BatchDataSourceScriptDatabaseInitializer customInitializer(DataSource dataSource, + BatchJdbcProperties properties) { + return new BatchDataSourceScriptDatabaseInitializer(dataSource, properties); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomDatabaseInitializerConfiguration { + + @Bean + DataSourceScriptDatabaseInitializer customInitializer(DataSource dataSource) { + return new DataSourceScriptDatabaseInitializer(dataSource, new DatabaseInitializationSettings()); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomBatchConfiguration extends DefaultBatchConfiguration { + + } + + @EnableBatchProcessing + @Configuration(proxyBeanMethods = false) + static class EnableBatchProcessingConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + static class ConversionServiceCustomizersConfiguration { + + @Bean + @Order(1) + BatchConversionServiceCustomizer batchConversionServiceCustomizer() { + return mock(BatchConversionServiceCustomizer.class); + } + + @Bean + @Order(2) + BatchConversionServiceCustomizer anotherBatchConversionServiceCustomizer() { + return mock(BatchConversionServiceCustomizer.class); + } + + } + +} diff --git a/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchAutoConfigurationWithoutJpaTests.java b/module/spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcAutoConfigurationWithoutJpaTests.java similarity index 85% rename from module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchAutoConfigurationWithoutJpaTests.java rename to module/spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcAutoConfigurationWithoutJpaTests.java index 38ca7439d62..c04e0a79fae 100644 --- a/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchAutoConfigurationWithoutJpaTests.java +++ b/module/spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcAutoConfigurationWithoutJpaTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.batch.autoconfigure; +package org.springframework.boot.batch.jdbc.autoconfigure; import javax.sql.DataSource; @@ -25,8 +25,8 @@ import org.springframework.batch.core.launch.JobOperator; import org.springframework.batch.core.repository.JobRepository; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; -import org.springframework.boot.batch.autoconfigure.BatchAutoConfiguration.SpringBootBatchConfiguration; -import org.springframework.boot.batch.autoconfigure.domain.City; +import org.springframework.boot.batch.jdbc.autoconfigure.BatchJdbcAutoConfiguration.SpringBootBatchJdbcConfiguration; +import org.springframework.boot.batch.jdbc.autoconfigure.domain.City; import org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration; import org.springframework.boot.jdbc.autoconfigure.EmbeddedDataSourceConfiguration; import org.springframework.boot.sql.init.DatabaseInitializationMode; @@ -40,15 +40,15 @@ import org.springframework.transaction.annotation.Isolation; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link BatchAutoConfiguration} when JPA is not on the classpath. + * Tests for {@link BatchJdbcAutoConfiguration} when JPA is not on the classpath. * * @author Stephane Nicoll */ @ClassPathExclusions("hibernate-jpa-*.jar") -class BatchAutoConfigurationWithoutJpaTests { +class BatchJdbcAutoConfigurationWithoutJpaTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(BatchAutoConfiguration.class, TransactionAutoConfiguration.class, + .withConfiguration(AutoConfigurations.of(BatchJdbcAutoConfiguration.class, TransactionAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class)); @Test @@ -58,7 +58,7 @@ class BatchAutoConfigurationWithoutJpaTests { .run((context) -> { assertThat(context).hasSingleBean(JobOperator.class); assertThat(context).hasSingleBean(JobRepository.class); - assertThat(context.getBean(BatchProperties.class).getJdbc().getInitializeSchema()) + assertThat(context.getBean(BatchJdbcProperties.class).getInitializeSchema()) .isEqualTo(DatabaseInitializationMode.EMBEDDED); assertThat(new JdbcTemplate(context.getBean(DataSource.class)) .queryForList("select * from BATCH_JOB_EXECUTION")).isEmpty(); @@ -89,7 +89,7 @@ class BatchAutoConfigurationWithoutJpaTests { .withPropertyValues("spring.datasource.generate-unique-name=true", "spring.batch.jdbc.isolation-level-for-create=read_committed") .run((context) -> assertThat( - context.getBean(SpringBootBatchConfiguration.class).getIsolationLevelForCreate()) + context.getBean(SpringBootBatchJdbcConfiguration.class).getIsolationLevelForCreate()) .isEqualTo(Isolation.READ_COMMITTED)); } diff --git a/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchPropertiesTests.java b/module/spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcPropertiesTests.java similarity index 85% rename from module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchPropertiesTests.java rename to module/spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcPropertiesTests.java index 7c19617cb74..cc511c748af 100644 --- a/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchPropertiesTests.java +++ b/module/spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/BatchJdbcPropertiesTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.batch.autoconfigure; +package org.springframework.boot.batch.jdbc.autoconfigure; import org.junit.jupiter.api.Test; @@ -23,15 +23,15 @@ import org.springframework.batch.core.configuration.support.JdbcDefaultBatchConf import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link BatchProperties}. + * Tests for {@link BatchJdbcProperties}. * * @author Andy Wilkinson */ -class BatchPropertiesTests { +class BatchJdbcPropertiesTests { @Test void validateTransactionStateDefaultMatchesSpringBatchDefault() { - assertThat(new BatchProperties().getJdbc().isValidateTransactionState()) + assertThat(new BatchJdbcProperties().isValidateTransactionState()) .isEqualTo(new TestBatchConfiguration().getValidateTransactionState()); } diff --git a/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/domain/City.java b/module/spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/domain/City.java similarity index 96% rename from module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/domain/City.java rename to module/spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/domain/City.java index b8ce06337ba..dedbe0e503e 100644 --- a/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/domain/City.java +++ b/module/spring-boot-batch-jdbc/src/test/java/org/springframework/boot/batch/jdbc/autoconfigure/domain/City.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.batch.autoconfigure.domain; +package org.springframework.boot.batch.jdbc.autoconfigure.domain; import java.io.Serializable; diff --git a/module/spring-boot-batch/src/test/resources/org/springframework/boot/batch/autoconfigure/custom-schema.sql b/module/spring-boot-batch-jdbc/src/test/resources/org/springframework/boot/batch/jdbc/autoconfigure/custom-schema.sql similarity index 100% rename from module/spring-boot-batch/src/test/resources/org/springframework/boot/batch/autoconfigure/custom-schema.sql rename to module/spring-boot-batch-jdbc/src/test/resources/org/springframework/boot/batch/jdbc/autoconfigure/custom-schema.sql diff --git a/module/spring-boot-batch/build.gradle b/module/spring-boot-batch/build.gradle index e3c68b7e73c..d2da8e0be5d 100644 --- a/module/spring-boot-batch/build.gradle +++ b/module/spring-boot-batch/build.gradle @@ -26,18 +26,12 @@ description = "Spring Boot Batch" dependencies { api(project(":core:spring-boot")) - api(project(":module:spring-boot-jdbc")) api("org.springframework.batch:spring-batch-core") - implementation(project(":module:spring-boot-tx")) - optional(project(":core:spring-boot-autoconfigure")) - optional(project(":module:spring-boot-hibernate")) optional(project(":module:spring-boot-micrometer-observation")) testImplementation(project(":core:spring-boot-test")) - testImplementation(project(":module:spring-boot-flyway")) - testImplementation(project(":module:spring-boot-liquibase")) testImplementation(project(":test-support:spring-boot-test-support")) testImplementation(testFixtures(project(":core:spring-boot-autoconfigure"))) testImplementation("io.micrometer:micrometer-observation-test") @@ -45,6 +39,4 @@ dependencies { testRuntimeOnly("ch.qos.logback:logback-classic") testRuntimeOnly("com.fasterxml.jackson.core:jackson-databind") testRuntimeOnly("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") - testRuntimeOnly("com.h2database:h2") - testRuntimeOnly("com.zaxxer:HikariCP") } diff --git a/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchAutoConfiguration.java b/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchAutoConfiguration.java index 62cc4157688..bf2dc30490c 100644 --- a/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchAutoConfiguration.java +++ b/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchAutoConfiguration.java @@ -16,163 +16,47 @@ package org.springframework.boot.batch.autoconfigure; -import java.util.List; - -import javax.sql.DataSource; - 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.JdbcDefaultBatchConfiguration; import org.springframework.batch.core.converter.JobParametersConverter; import org.springframework.batch.core.launch.JobOperator; -import org.springframework.batch.core.repository.ExecutionContextSerializer; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.ExitCodeGenerator; 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.ConditionalOnBooleanProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration; -import org.springframework.boot.sql.autoconfigure.init.OnDatabaseInitializationCondition; -import org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer; -import org.springframework.boot.transaction.autoconfigure.TransactionAutoConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.task.TaskExecutor; -import org.springframework.jdbc.datasource.init.DatabasePopulator; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.annotation.Isolation; -import org.springframework.util.StringUtils; /** - * {@link EnableAutoConfiguration Auto-configuration} for Spring Batch. If a single job is - * found in the context, it will be executed on startup. - *

- * Disable this behavior with {@literal spring.batch.job.enabled=false}). - *

- * If multiple jobs are found, a job name to execute on startup can be supplied by the - * User with : {@literal spring.batch.job.name=job1}. In this case the Runner will first - * find jobs registered as Beans, then those in the existing JobRegistry. + * {@link EnableAutoConfiguration Auto-configuration} for Spring Batch using an in-memory + * store. * - * @author Dave Syer - * @author EddĂș MelĂ©ndez - * @author Kazuki Shimizu - * @author Mahmoud Ben Hassine - * @author Lars Uffmann - * @author Lasse Wulff - * @author Yanming Zhou + * @author Stephane Nicoll * @since 4.0.0 */ -@AutoConfiguration(after = { DataSourceAutoConfiguration.class, TransactionAutoConfiguration.class }, - afterName = "org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration") -@ConditionalOnClass({ JobOperator.class, DataSource.class, DatabasePopulator.class }) -@ConditionalOnBean({ DataSource.class, PlatformTransactionManager.class }) +@AutoConfiguration +@ConditionalOnClass(JobOperator.class) @ConditionalOnMissingBean(value = DefaultBatchConfiguration.class, annotation = EnableBatchProcessing.class) @EnableConfigurationProperties(BatchProperties.class) -@Import(DatabaseInitializationDependencyConfigurer.class) public final class BatchAutoConfiguration { - @Bean - @ConditionalOnMissingBean - @ConditionalOnBooleanProperty(name = "spring.batch.job.enabled", matchIfMissing = true) - JobLauncherApplicationRunner jobLauncherApplicationRunner(JobOperator jobOperator, BatchProperties properties) { - JobLauncherApplicationRunner runner = new JobLauncherApplicationRunner(jobOperator); - String jobName = properties.getJob().getName(); - if (StringUtils.hasText(jobName)) { - runner.setJobName(jobName); - } - return runner; - } - - @Bean - @ConditionalOnMissingBean(ExitCodeGenerator.class) - JobExecutionExitCodeGenerator jobExecutionExitCodeGenerator() { - return new JobExecutionExitCodeGenerator(); - } - @Configuration(proxyBeanMethods = false) - static class SpringBootBatchConfiguration extends JdbcDefaultBatchConfiguration { - - private final DataSource dataSource; - - private final PlatformTransactionManager transactionManager; + static class SpringBootBatchDefaultConfiguration extends DefaultBatchConfiguration { private final @Nullable TaskExecutor taskExecutor; - private final BatchProperties properties; - - private final List batchConversionServiceCustomizers; - - private final @Nullable ExecutionContextSerializer executionContextSerializer; - private final @Nullable JobParametersConverter jobParametersConverter; - SpringBootBatchConfiguration(DataSource dataSource, @BatchDataSource ObjectProvider batchDataSource, - PlatformTransactionManager transactionManager, - @BatchTransactionManager ObjectProvider batchTransactionManager, - @BatchTaskExecutor ObjectProvider batchTaskExecutor, BatchProperties properties, - ObjectProvider batchConversionServiceCustomizers, - ObjectProvider executionContextSerializer, + SpringBootBatchDefaultConfiguration(@BatchTaskExecutor ObjectProvider batchTaskExecutor, ObjectProvider jobParametersConverter) { - this.dataSource = batchDataSource.getIfAvailable(() -> dataSource); - this.transactionManager = batchTransactionManager.getIfAvailable(() -> transactionManager); this.taskExecutor = batchTaskExecutor.getIfAvailable(); - this.properties = properties; - this.batchConversionServiceCustomizers = batchConversionServiceCustomizers.orderedStream().toList(); - this.executionContextSerializer = executionContextSerializer.getIfAvailable(); this.jobParametersConverter = jobParametersConverter.getIfAvailable(); } - @Override - protected DataSource getDataSource() { - return this.dataSource; - } - - @Override - protected PlatformTransactionManager getTransactionManager() { - return this.transactionManager; - } - - @Override - protected String getTablePrefix() { - String tablePrefix = this.properties.getJdbc().getTablePrefix(); - return (tablePrefix != null) ? tablePrefix : super.getTablePrefix(); - } - - @Override - protected boolean getValidateTransactionState() { - return this.properties.getJdbc().isValidateTransactionState(); - } - - @Override - protected Isolation getIsolationLevelForCreate() { - Isolation isolation = this.properties.getJdbc().getIsolationLevelForCreate(); - return (isolation != null) ? isolation : super.getIsolationLevelForCreate(); - } - - @Override - protected ConfigurableConversionService getConversionService() { - ConfigurableConversionService conversionService = super.getConversionService(); - for (BatchConversionServiceCustomizer customizer : this.batchConversionServiceCustomizers) { - customizer.customize(conversionService); - } - return conversionService; - } - - @Override - protected ExecutionContextSerializer getExecutionContextSerializer() { - return (this.executionContextSerializer != null) ? this.executionContextSerializer - : super.getExecutionContextSerializer(); - } - @Override @Deprecated(since = "4.0.0", forRemoval = true) @SuppressWarnings("removal") @@ -188,26 +72,4 @@ public final class BatchAutoConfiguration { } - @Configuration(proxyBeanMethods = false) - @Conditional(OnBatchDatasourceInitializationCondition.class) - static class DataSourceInitializerConfiguration { - - @Bean - @ConditionalOnMissingBean - BatchDataSourceScriptDatabaseInitializer batchDataSourceInitializer(DataSource dataSource, - @BatchDataSource ObjectProvider batchDataSource, BatchProperties properties) { - return new BatchDataSourceScriptDatabaseInitializer(batchDataSource.getIfAvailable(() -> dataSource), - properties.getJdbc()); - } - - } - - static class OnBatchDatasourceInitializationCondition extends OnDatabaseInitializationCondition { - - OnBatchDatasourceInitializationCondition() { - super("Batch", "spring.batch.jdbc.initialize-schema"); - } - - } - } diff --git a/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchJobLauncherAutoConfiguration.java b/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchJobLauncherAutoConfiguration.java new file mode 100644 index 00000000000..bc9fc5ec3a5 --- /dev/null +++ b/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchJobLauncherAutoConfiguration.java @@ -0,0 +1,74 @@ +/* + * 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.autoconfigure; + +import org.springframework.batch.core.launch.JobOperator; +import org.springframework.boot.ExitCodeGenerator; +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.ConditionalOnBooleanProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.util.StringUtils; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Spring Batch. If a single job is + * found in the context, it will be executed on startup. + *

+ * Disable this behavior with {@literal spring.batch.job.enabled=false}). + *

+ * If multiple jobs are found, a job name to execute on startup can be supplied by the + * User with : {@literal spring.batch.job.name=job1}. In this case the Runner will first + * find jobs registered as Beans, then those in the existing JobRegistry. + * + * @author Dave Syer + * @author EddĂș MelĂ©ndez + * @author Kazuki Shimizu + * @author Mahmoud Ben Hassine + * @author Lars Uffmann + * @author Lasse Wulff + * @author Yanming Zhou + * @since 4.0.0 + */ +@AutoConfiguration(after = BatchAutoConfiguration.class) +@ConditionalOnClass(JobOperator.class) +@ConditionalOnBean(JobOperator.class) +@EnableConfigurationProperties(BatchProperties.class) +public final class BatchJobLauncherAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnBooleanProperty(name = "spring.batch.job.enabled", matchIfMissing = true) + JobLauncherApplicationRunner jobLauncherApplicationRunner(JobOperator jobOperator, BatchProperties properties) { + JobLauncherApplicationRunner runner = new JobLauncherApplicationRunner(jobOperator); + String jobName = properties.getJob().getName(); + if (StringUtils.hasText(jobName)) { + runner.setJobName(jobName); + } + return runner; + } + + @Bean + @ConditionalOnMissingBean(ExitCodeGenerator.class) + JobExecutionExitCodeGenerator jobExecutionExitCodeGenerator() { + return new JobExecutionExitCodeGenerator(); + } + +} diff --git a/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchProperties.java b/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchProperties.java index 822a10b5a4a..81f05be18d1 100644 --- a/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchProperties.java +++ b/module/spring-boot-batch/src/main/java/org/springframework/boot/batch/autoconfigure/BatchProperties.java @@ -16,11 +16,7 @@ package org.springframework.boot.batch.autoconfigure; -import org.jspecify.annotations.Nullable; - import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.sql.init.DatabaseInitializationMode; -import org.springframework.transaction.annotation.Isolation; /** * Configuration properties for Spring Batch. @@ -37,16 +33,10 @@ public class BatchProperties { private final Job job = new Job(); - private final Jdbc jdbc = new Jdbc(); - public Job getJob() { return this.job; } - public Jdbc getJdbc() { - return this.jdbc; - } - public static class Job { /** @@ -65,90 +55,4 @@ public class BatchProperties { } - public static class Jdbc { - - private static final String DEFAULT_SCHEMA_LOCATION = "classpath:org/springframework/" - + "batch/core/schema-@@platform@@.sql"; - - /** - * Whether to validate the transaction state. - */ - private boolean validateTransactionState = true; - - /** - * Transaction isolation level to use when creating job meta-data for new jobs. - */ - private @Nullable Isolation isolationLevelForCreate; - - /** - * Path to the SQL file to use to initialize the database schema. - */ - private String schema = DEFAULT_SCHEMA_LOCATION; - - /** - * Platform to use in initialization scripts if the @@platform@@ placeholder is - * used. Auto-detected by default. - */ - private @Nullable String platform; - - /** - * Table prefix for all the batch meta-data tables. - */ - private @Nullable String tablePrefix; - - /** - * Database schema initialization mode. - */ - private DatabaseInitializationMode initializeSchema = DatabaseInitializationMode.EMBEDDED; - - 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 String getSchema() { - return this.schema; - } - - public void setSchema(String schema) { - this.schema = schema; - } - - public @Nullable String getPlatform() { - return this.platform; - } - - public void setPlatform(@Nullable String platform) { - this.platform = platform; - } - - public @Nullable String getTablePrefix() { - return this.tablePrefix; - } - - public void setTablePrefix(@Nullable String tablePrefix) { - this.tablePrefix = tablePrefix; - } - - public DatabaseInitializationMode getInitializeSchema() { - return this.initializeSchema; - } - - public void setInitializeSchema(DatabaseInitializationMode initializeSchema) { - this.initializeSchema = initializeSchema; - } - - } - } diff --git a/module/spring-boot-batch/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-batch/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 414dd76ca42..2a4b792750f 100644 --- a/module/spring-boot-batch/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/module/spring-boot-batch/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1,43 +1,10 @@ { "properties": [ - { - "name": "spring.batch.initialize-schema", - "type": "org.springframework.boot.sql.init.DatabaseInitializationMode", - "deprecation": { - "replacement": "spring.batch.jdbc.initialize-schema", - "level": "error" - } - }, - { - "name": "spring.batch.initializer.enabled", - "type": "java.lang.Boolean", - "description": "Create the required batch tables on startup if necessary. Enabled automatically\n if no custom table prefix is set or if a custom schema is configured.", - "deprecation": { - "replacement": "spring.batch.jdbc.initialize-schema", - "level": "error" - } - }, { "name": "spring.batch.job.enabled", "type": "java.lang.Boolean", "description": "Whether to execute a Spring Batch job on startup. When multiple jobs are present in the context, set spring.batch.job.name to identify the job to execute.", "defaultValue": true - }, - { - "name": "spring.batch.schema", - "type": "java.lang.String", - "deprecation": { - "replacement": "spring.batch.jdbc.schema", - "level": "error" - } - }, - { - "name": "spring.batch.table-prefix", - "type": "java.lang.String", - "deprecation": { - "replacement": "spring.batch.jdbc.table-prefix", - "level": "error" - } } ] } \ No newline at end of file diff --git a/module/spring-boot-batch/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-batch/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 6f6b4d9629a..66bc88e0a79 100644 --- a/module/spring-boot-batch/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/module/spring-boot-batch/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,2 +1,3 @@ org.springframework.boot.batch.autoconfigure.BatchAutoConfiguration +org.springframework.boot.batch.autoconfigure.BatchJobLauncherAutoConfiguration org.springframework.boot.batch.autoconfigure.observation.BatchObservationAutoConfiguration diff --git a/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchAutoConfigurationTests.java b/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchAutoConfigurationTests.java index 98ba0a6871d..ce5264579a6 100644 --- a/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchAutoConfigurationTests.java +++ b/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchAutoConfigurationTests.java @@ -17,86 +17,42 @@ package org.springframework.boot.batch.autoconfigure; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; -import javax.sql.DataSource; - -import jakarta.persistence.EntityManagerFactory; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InOrder; -import org.mockito.Mockito; -import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.configuration.JobRegistry; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration; import org.springframework.batch.core.converter.DefaultJobParametersConverter; import org.springframework.batch.core.converter.JobParametersConverter; import org.springframework.batch.core.converter.JsonJobParametersConverter; -import org.springframework.batch.core.job.AbstractJob; import org.springframework.batch.core.job.Job; -import org.springframework.batch.core.job.JobExecution; -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.ExecutionContextSerializer; import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.repository.dao.DefaultExecutionContextSerializer; -import org.springframework.batch.core.repository.dao.Jackson2ExecutionContextStringSerializer; -import org.springframework.batch.core.step.Step; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.boot.CommandLineRunner; -import org.springframework.boot.DefaultApplicationArguments; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; -import org.springframework.boot.batch.autoconfigure.BatchAutoConfiguration.SpringBootBatchConfiguration; -import org.springframework.boot.batch.autoconfigure.domain.City; -import org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration; -import org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration; -import org.springframework.boot.jdbc.DataSourceBuilder; -import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration; -import org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration; -import org.springframework.boot.jdbc.autoconfigure.EmbeddedDataSourceConfiguration; -import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; -import org.springframework.boot.liquibase.autoconfigure.LiquibaseAutoConfiguration; -import org.springframework.boot.sql.init.DatabaseInitializationMode; -import org.springframework.boot.sql.init.DatabaseInitializationSettings; +import org.springframework.boot.batch.autoconfigure.BatchAutoConfiguration.SpringBootBatchDefaultConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.system.OutputCaptureExtension; -import org.springframework.boot.testsupport.classpath.resources.WithPackageResources; -import org.springframework.boot.testsupport.classpath.resources.WithResource; -import org.springframework.boot.transaction.autoconfigure.TransactionAutoConfiguration; -import org.springframework.boot.transaction.autoconfigure.TransactionManagerCustomizationAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; import org.springframework.core.annotation.Order; -import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.core.task.SyncTaskExecutor; import org.springframework.core.task.TaskExecutor; -import org.springframework.jdbc.BadSqlGrammarException; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.datasource.init.DatabasePopulator; -import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.test.util.AopTestUtils; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.annotation.Isolation; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** - * Tests for {@link BatchAutoConfiguration}. + * Tests for {@link BatchJobLauncherAutoConfiguration}. * * @author Dave Syer * @author Stephane Nicoll @@ -110,336 +66,43 @@ import static org.mockito.Mockito.mock; @ExtendWith(OutputCaptureExtension.class) class BatchAutoConfigurationTests { - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( - AutoConfigurations.of(BatchAutoConfiguration.class, TransactionManagerCustomizationAutoConfiguration.class, - TransactionAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class)); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(BatchAutoConfiguration.class)); @Test void testDefaultContext() { - this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class).run((context) -> { + this.contextRunner.run((context) -> { assertThat(context).hasSingleBean(JobRepository.class); assertThat(context).hasSingleBean(JobOperator.class); - assertThat(context.getBean(BatchProperties.class).getJdbc().getInitializeSchema()) - .isEqualTo(DatabaseInitializationMode.EMBEDDED); - assertThat(new JdbcTemplate(context.getBean(DataSource.class)) - .queryForList("select * from BATCH_JOB_EXECUTION")).isEmpty(); }); } - @Test - void autoconfigurationBacksOffEntirelyIfSpringJdbcAbsent() { - this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withClassLoader(new FilteredClassLoader(DatabasePopulator.class)) - .run((context) -> { - assertThat(context).doesNotHaveBean(JobLauncherApplicationRunner.class); - assertThat(context).doesNotHaveBean(BatchDataSourceScriptDatabaseInitializer.class); - }); - } - @Test void autoConfigurationBacksOffWhenUserEnablesBatchProcessing() { - this.contextRunner - .withUserConfiguration(EnableBatchProcessingConfiguration.class, EmbeddedDataSourceConfiguration.class) - .withClassLoader(new FilteredClassLoader(DatabasePopulator.class)) - .run((context) -> assertThat(context).doesNotHaveBean(SpringBootBatchConfiguration.class)); + this.contextRunner.withUserConfiguration(EnableBatchProcessingConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(SpringBootBatchDefaultConfiguration.class)); } @Test void autoConfigurationBacksOffWhenUserProvidesBatchConfiguration() { - this.contextRunner.withUserConfiguration(CustomBatchConfiguration.class, EmbeddedDataSourceConfiguration.class) + this.contextRunner.withUserConfiguration(CustomBatchConfiguration.class) .withClassLoader(new FilteredClassLoader(DatabasePopulator.class)) - .run((context) -> assertThat(context).doesNotHaveBean(SpringBootBatchConfiguration.class)); - } - - @Test - void testDefinesAndLaunchesJob() { - this.contextRunner.withUserConfiguration(JobConfiguration.class, EmbeddedDataSourceConfiguration.class) - .run((context) -> { - assertThat(context).hasSingleBean(JobOperator.class); - context.getBean(JobLauncherApplicationRunner.class) - .run(new DefaultApplicationArguments("jobParam=test")); - JobParameters jobParameters = new JobParametersBuilder().addString("jobParam", "test") - .toJobParameters(); - assertThat(context.getBean(JobRepository.class).getLastJobExecution("job", jobParameters)).isNotNull(); - }); - } - - @Test - void testDefinesAndLaunchesJobIgnoreOptionArguments() { - this.contextRunner.withUserConfiguration(JobConfiguration.class, EmbeddedDataSourceConfiguration.class) - .run((context) -> { - assertThat(context).hasSingleBean(JobOperator.class); - context.getBean(JobLauncherApplicationRunner.class) - .run(new DefaultApplicationArguments("--spring.property=value", "jobParam=test")); - JobParameters jobParameters = new JobParametersBuilder().addString("jobParam", "test") - .toJobParameters(); - assertThat(context.getBean(JobRepository.class).getLastJobExecution("job", jobParameters)).isNotNull(); - }); - } - - @Test - void testRegisteredAndLocalJob() { - this.contextRunner - .withUserConfiguration(NamedJobConfigurationWithRegisteredAndLocalJob.class, - EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.batch.job.name:discreteRegisteredJob") - .run((context) -> { - assertThat(context).hasSingleBean(JobOperator.class); - context.getBean(JobLauncherApplicationRunner.class).run(); - assertThat(context.getBean(JobRepository.class) - .getLastJobExecution("discreteRegisteredJob", new JobParameters()) - .getStatus()).isEqualTo(BatchStatus.COMPLETED); - }); - } - - @Test - void testDefinesAndLaunchesLocalJob() { - this.contextRunner - .withUserConfiguration(NamedJobConfigurationWithLocalJob.class, EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.batch.job.name:discreteLocalJob") - .run((context) -> { - assertThat(context).hasSingleBean(JobOperator.class); - context.getBean(JobLauncherApplicationRunner.class).run(); - assertThat(context.getBean(JobRepository.class) - .getLastJobExecution("discreteLocalJob", new JobParameters())).isNotNull(); - }); - } - - @Test - void testMultipleJobsAndNoJobName() { - this.contextRunner.withUserConfiguration(MultipleJobConfiguration.class, EmbeddedDataSourceConfiguration.class) - .run((context) -> { - assertThat(context).hasFailed(); - assertThat(context.getStartupFailure().getCause().getMessage()) - .contains("Job name must be specified in case of multiple jobs"); - }); - } - - @Test - void testMultipleJobsAndJobName() { - this.contextRunner.withUserConfiguration(MultipleJobConfiguration.class, EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.batch.job.name:discreteLocalJob") - .run((context) -> { - assertThat(context).hasSingleBean(JobOperator.class); - context.getBean(JobLauncherApplicationRunner.class).run(); - assertThat(context.getBean(JobRepository.class) - .getLastJobExecution("discreteLocalJob", new JobParameters())).isNotNull(); - }); - } - - @Test - void testDisableLaunchesJob() { - this.contextRunner.withUserConfiguration(JobConfiguration.class, EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.batch.job.enabled:false") - .run((context) -> { - assertThat(context).hasSingleBean(JobOperator.class); - assertThat(context).doesNotHaveBean(CommandLineRunner.class); - }); - } - - @Test - void testDisableSchemaLoader() { - this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.datasource.generate-unique-name=true", - "spring.batch.jdbc.initialize-schema:never") - .run((context) -> { - assertThat(context).hasSingleBean(JobOperator.class); - assertThat(context.getBean(BatchProperties.class).getJdbc().getInitializeSchema()) - .isEqualTo(DatabaseInitializationMode.NEVER); - assertThat(context).doesNotHaveBean(BatchDataSourceScriptDatabaseInitializer.class); - assertThatExceptionOfType(BadSqlGrammarException.class) - .isThrownBy(() -> new JdbcTemplate(context.getBean(DataSource.class)) - .queryForList("select * from BATCH_JOB_EXECUTION")); - }); - } - - @Test - void testUsingJpa() { - this.contextRunner - .withUserConfiguration(TestJpaConfiguration.class, EmbeddedDataSourceConfiguration.class, - HibernateJpaAutoConfiguration.class) - .run((context) -> { - PlatformTransactionManager transactionManager = context.getBean(PlatformTransactionManager.class); - // It's a lazy proxy, but it does render its target if you ask for - // toString(): - assertThat(transactionManager.toString()).contains("JpaTransactionManager"); - assertThat(context).hasSingleBean(EntityManagerFactory.class); - // Ensure the JobRepository can be used (no problem with isolation - // level) - assertThat(context.getBean(JobRepository.class).getLastJobExecution("job", new JobParameters())) - .isNull(); - }); - } - - @Test - @WithPackageResources("custom-schema.sql") - void testRenamePrefix() { - this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.datasource.generate-unique-name=true", - "spring.batch.jdbc.schema:classpath:custom-schema.sql", "spring.batch.jdbc.table-prefix:PREFIX_") - .run((context) -> { - assertThat(context).hasSingleBean(JobOperator.class); - assertThat(context.getBean(BatchProperties.class).getJdbc().getInitializeSchema()) - .isEqualTo(DatabaseInitializationMode.EMBEDDED); - assertThat(new JdbcTemplate(context.getBean(DataSource.class)) - .queryForList("select * from PREFIX_JOB_EXECUTION")).isEmpty(); - JobRepository jobRepository = context.getBean(JobRepository.class); - assertThat(jobRepository.findRunningJobExecutions("test")).isEmpty(); - assertThat(jobRepository.getLastJobExecution("test", new JobParameters())).isNull(); - }); - } - - @Test - void testCustomizeJpaTransactionManagerUsingProperties() { - this.contextRunner - .withUserConfiguration(TestJpaConfiguration.class, EmbeddedDataSourceConfiguration.class, - HibernateJpaAutoConfiguration.class) - .withPropertyValues("spring.transaction.default-timeout:30", - "spring.transaction.rollback-on-commit-failure:true") - .run((context) -> { - assertThat(context).hasSingleBean(BatchAutoConfiguration.class); - JpaTransactionManager transactionManager = JpaTransactionManager.class - .cast(context.getBean(SpringBootBatchConfiguration.class).getTransactionManager()); - assertThat(transactionManager.getDefaultTimeout()).isEqualTo(30); - assertThat(transactionManager.isRollbackOnCommitFailure()).isTrue(); - }); - } - - @Test - void testCustomizeDataSourceTransactionManagerUsingProperties() { - this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.transaction.default-timeout:30", - "spring.transaction.rollback-on-commit-failure:true") - .run((context) -> { - assertThat(context).hasSingleBean(SpringBootBatchConfiguration.class); - DataSourceTransactionManager transactionManager = DataSourceTransactionManager.class - .cast(context.getBean(SpringBootBatchConfiguration.class).getTransactionManager()); - assertThat(transactionManager.getDefaultTimeout()).isEqualTo(30); - assertThat(transactionManager.isRollbackOnCommitFailure()).isTrue(); - }); - } - - @Test - void testBatchDataSource() { - this.contextRunner.withUserConfiguration(BatchDataSourceConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(SpringBootBatchConfiguration.class) - .hasSingleBean(BatchDataSourceScriptDatabaseInitializer.class) - .hasBean("batchDataSource"); - DataSource batchDataSource = context.getBean("batchDataSource", DataSource.class); - assertThat(context.getBean(SpringBootBatchConfiguration.class).getDataSource()).isEqualTo(batchDataSource); - assertThat(context.getBean(BatchDataSourceScriptDatabaseInitializer.class)) - .hasFieldOrPropertyWithValue("dataSource", batchDataSource); - }); - } - - @Test - void testBatchTransactionManager() { - this.contextRunner.withUserConfiguration(BatchTransactionManagerConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(SpringBootBatchConfiguration.class); - PlatformTransactionManager batchTransactionManager = context.getBean("batchTransactionManager", - PlatformTransactionManager.class); - assertThat(context.getBean(SpringBootBatchConfiguration.class).getTransactionManager()) - .isEqualTo(batchTransactionManager); - }); + .run((context) -> assertThat(context).doesNotHaveBean(SpringBootBatchDefaultConfiguration.class)); } @Test void testBatchTaskExecutor() { - this.contextRunner - .withUserConfiguration(BatchTaskExecutorConfiguration.class, EmbeddedDataSourceConfiguration.class) - .run((context) -> { - assertThat(context).hasSingleBean(SpringBootBatchConfiguration.class).hasBean("batchTaskExecutor"); - TaskExecutor batchTaskExecutor = context.getBean("batchTaskExecutor", TaskExecutor.class); - assertThat(batchTaskExecutor).isInstanceOf(AsyncTaskExecutor.class); - assertThat(context.getBean(SpringBootBatchConfiguration.class).getTaskExecutor()) - .isEqualTo(batchTaskExecutor); - JobOperator jobOperator = AopTestUtils.getTargetObject(context.getBean(JobOperator.class)); - assertThat(jobOperator).hasFieldOrPropertyWithValue("taskExecutor", batchTaskExecutor); - }); - } - - @Test - void jobRepositoryBeansDependOnBatchDataSourceInitializer() { - this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class).run((context) -> { - ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); - String[] jobRepositoryNames = beanFactory.getBeanNamesForType(JobRepository.class); - assertThat(jobRepositoryNames).isNotEmpty(); - for (String jobRepositoryName : jobRepositoryNames) { - assertThat(beanFactory.getBeanDefinition(jobRepositoryName).getDependsOn()) - .contains("batchDataSourceInitializer"); - } + this.contextRunner.withUserConfiguration(BatchTaskExecutorConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(SpringBootBatchDefaultConfiguration.class).hasBean("batchTaskExecutor"); + TaskExecutor batchTaskExecutor = context.getBean("batchTaskExecutor", TaskExecutor.class); + assertThat(batchTaskExecutor).isInstanceOf(AsyncTaskExecutor.class); + assertThat(context.getBean(SpringBootBatchDefaultConfiguration.class).getTaskExecutor()) + .isEqualTo(batchTaskExecutor); + JobOperator jobOperator = AopTestUtils.getTargetObject(context.getBean(JobOperator.class)); + assertThat(jobOperator).hasFieldOrPropertyWithValue("taskExecutor", batchTaskExecutor); }); } - @Test - void jobRepositoryBeansDependOnFlyway() { - this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class, FlywayAutoConfiguration.class) - .withPropertyValues("spring.batch.jdbc.initialize-schema=never") - .run((context) -> { - ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); - String[] jobRepositoryNames = beanFactory.getBeanNamesForType(JobRepository.class); - assertThat(jobRepositoryNames).isNotEmpty(); - for (String jobRepositoryName : jobRepositoryNames) { - assertThat(beanFactory.getBeanDefinition(jobRepositoryName).getDependsOn()).contains("flyway", - "flywayInitializer"); - } - }); - } - - @Test - @WithResource(name = "db/changelog/db.changelog-master.yaml", content = "databaseChangeLog:") - void jobRepositoryBeansDependOnLiquibase() { - this.contextRunner - .withUserConfiguration(EmbeddedDataSourceConfiguration.class, LiquibaseAutoConfiguration.class) - .withPropertyValues("spring.batch.jdbc.initialize-schema=never") - .run((context) -> { - ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); - String[] jobRepositoryNames = beanFactory.getBeanNamesForType(JobRepository.class); - assertThat(jobRepositoryNames).isNotEmpty(); - for (String jobRepositoryName : jobRepositoryNames) { - assertThat(beanFactory.getBeanDefinition(jobRepositoryName).getDependsOn()).contains("liquibase"); - } - }); - } - - @Test - void whenTheUserDefinesTheirOwnBatchDatabaseInitializerThenTheAutoConfiguredInitializerBacksOff() { - this.contextRunner.withUserConfiguration(CustomBatchDatabaseInitializerConfiguration.class) - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - DataSourceTransactionManagerAutoConfiguration.class)) - .run((context) -> assertThat(context).hasSingleBean(BatchDataSourceScriptDatabaseInitializer.class) - .doesNotHaveBean("batchDataSourceScriptDatabaseInitializer") - .hasBean("customInitializer")); - } - - @Test - void whenTheUserDefinesTheirOwnDatabaseInitializerThenTheAutoConfiguredBatchInitializerRemains() { - this.contextRunner.withUserConfiguration(CustomDatabaseInitializerConfiguration.class) - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - DataSourceTransactionManagerAutoConfiguration.class)) - .run((context) -> assertThat(context).hasSingleBean(BatchDataSourceScriptDatabaseInitializer.class) - .hasBean("customInitializer")); - } - - @Test - void conversionServiceCustomizersAreCalled() { - this.contextRunner - .withUserConfiguration(EmbeddedDataSourceConfiguration.class, - ConversionServiceCustomizersConfiguration.class) - .run((context) -> { - BatchConversionServiceCustomizer customizer = context.getBean("batchConversionServiceCustomizer", - BatchConversionServiceCustomizer.class); - BatchConversionServiceCustomizer anotherCustomizer = context - .getBean("anotherBatchConversionServiceCustomizer", BatchConversionServiceCustomizer.class); - InOrder inOrder = Mockito.inOrder(customizer, anotherCustomizer); - ConfigurableConversionService configurableConversionService = context - .getBean(SpringBootBatchConfiguration.class) - .getConversionService(); - inOrder.verify(customizer).customize(configurableConversionService); - inOrder.verify(anotherCustomizer).customize(configurableConversionService); - }); - } - @Test void whenTheUserDefinesAJobNameAsJobInstanceValidates() { JobLauncherApplicationRunner runner = createInstance("another"); @@ -472,60 +135,24 @@ class BatchAutoConfigurationTests { .withMessage("No job found with name 'three'"); } - @Test - void customExecutionContextSerializerIsUsed() { - this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withBean(ExecutionContextSerializer.class, Jackson2ExecutionContextStringSerializer::new) - .run((context) -> { - assertThat(context).hasSingleBean(Jackson2ExecutionContextStringSerializer.class); - assertThat(context.getBean(SpringBootBatchConfiguration.class).getExecutionContextSerializer()) - .isInstanceOf(Jackson2ExecutionContextStringSerializer.class); - }); - } - - @Test - void defaultExecutionContextSerializerIsUsed() { - this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class).run((context) -> { - assertThat(context).doesNotHaveBean(ExecutionContextSerializer.class); - assertThat(context.getBean(SpringBootBatchConfiguration.class).getExecutionContextSerializer()) - .isInstanceOf(DefaultExecutionContextSerializer.class); - }); - } - - @Test - void customJdbcPropertiesIsUsed() { - this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.batch.jdbc.validate-transaction-state:false", - "spring.batch.jdbc.isolation-level-for-create:READ_COMMITTED") - .run((context) -> { - SpringBootBatchConfiguration configuration = context.getBean(SpringBootBatchConfiguration.class); - assertThat(configuration.getValidateTransactionState()).isEqualTo(false); - assertThat(configuration.getIsolationLevelForCreate()).isEqualTo(Isolation.READ_COMMITTED); - }); - - } - @Test @Deprecated(since = "4.0.0", forRemoval = true) @SuppressWarnings("removal") void customJobParametersConverterIsUsed() { - this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withBean(JobParametersConverter.class, JsonJobParametersConverter::new) - .withPropertyValues("spring.datasource.generate-unique-name=true") - .run((context) -> { - assertThat(context).hasSingleBean(JsonJobParametersConverter.class); - assertThat(context.getBean(SpringBootBatchConfiguration.class).getJobParametersConverter()) - .isInstanceOf(JsonJobParametersConverter.class); - }); + this.contextRunner.withBean(JobParametersConverter.class, JsonJobParametersConverter::new).run((context) -> { + assertThat(context).hasSingleBean(JsonJobParametersConverter.class); + assertThat(context.getBean(SpringBootBatchDefaultConfiguration.class).getJobParametersConverter()) + .isInstanceOf(JsonJobParametersConverter.class); + }); } @Test @Deprecated(since = "4.0.0", forRemoval = true) @SuppressWarnings("removal") void defaultJobParametersConverterIsUsed() { - this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class).run((context) -> { + this.contextRunner.run((context) -> { assertThat(context).doesNotHaveBean(JobParametersConverter.class); - assertThat(context.getBean(SpringBootBatchConfiguration.class).getJobParametersConverter()) + assertThat(context.getBean(SpringBootBatchDefaultConfiguration.class).getJobParametersConverter()) .isInstanceOf(DefaultJobParametersConverter.class); }); } @@ -544,44 +171,6 @@ class BatchAutoConfigurationTests { return job; } - @Configuration(proxyBeanMethods = false) - static class BatchDataSourceConfiguration { - - @Bean - DataSource normalDataSource() { - return DataSourceBuilder.create().url("jdbc:h2:mem:normal").username("sa").build(); - } - - @BatchDataSource - @Bean(defaultCandidate = false) - DataSource batchDataSource() { - return DataSourceBuilder.create().url("jdbc:h2:mem:batchdatasource").username("sa").build(); - } - - } - - @Configuration(proxyBeanMethods = false) - static class BatchTransactionManagerConfiguration { - - @Bean - DataSource dataSource() { - return DataSourceBuilder.create().url("jdbc:h2:mem:database").username("sa").build(); - } - - @Bean - @Primary - PlatformTransactionManager normalTransactionManager() { - return mock(PlatformTransactionManager.class); - } - - @BatchTransactionManager - @Bean(defaultCandidate = false) - PlatformTransactionManager batchTransactionManager() { - return mock(PlatformTransactionManager.class); - } - - } - @Configuration(proxyBeanMethods = false) static class BatchTaskExecutorConfiguration { @@ -603,188 +192,6 @@ class BatchAutoConfigurationTests { } - @TestAutoConfigurationPackage(City.class) - static class TestJpaConfiguration { - - } - - @Configuration(proxyBeanMethods = false) - static class EntityManagerFactoryConfiguration { - - @Bean - EntityManagerFactory entityManagerFactory() { - return mock(EntityManagerFactory.class); - } - - } - - @Configuration(proxyBeanMethods = false) - static class NamedJobConfigurationWithRegisteredAndLocalJob { - - @Autowired - private JobRepository jobRepository; - - @Bean - Job discreteJob() { - AbstractJob job = new AbstractJob("discreteRegisteredJob") { - - private static int count = 0; - - @Override - public Collection getStepNames() { - return Collections.emptySet(); - } - - @Override - public Step getStep(String stepName) { - return null; - } - - @Override - protected void doExecute(JobExecution execution) { - if (count == 0) { - execution.setStatus(BatchStatus.COMPLETED); - } - else { - execution.setStatus(BatchStatus.FAILED); - } - count++; - } - }; - job.setJobRepository(this.jobRepository); - return job; - } - - } - - @Configuration(proxyBeanMethods = false) - static class NamedJobConfigurationWithLocalJob { - - @Autowired - private JobRepository jobRepository; - - @Bean - Job discreteJob() { - AbstractJob job = new AbstractJob("discreteLocalJob") { - - @Override - public Collection getStepNames() { - return Collections.emptySet(); - } - - @Override - public Step getStep(String stepName) { - return null; - } - - @Override - protected void doExecute(JobExecution execution) { - execution.setStatus(BatchStatus.COMPLETED); - } - }; - job.setJobRepository(this.jobRepository); - return job; - } - - } - - @Configuration(proxyBeanMethods = false) - static class MultipleJobConfiguration { - - @Autowired - private JobRepository jobRepository; - - @Bean - Job discreteJob() { - AbstractJob job = new AbstractJob("discreteLocalJob") { - - @Override - public Collection getStepNames() { - return Collections.emptySet(); - } - - @Override - public Step getStep(String stepName) { - return null; - } - - @Override - protected void doExecute(JobExecution execution) { - execution.setStatus(BatchStatus.COMPLETED); - } - }; - job.setJobRepository(this.jobRepository); - return job; - } - - @Bean - Job job2() { - return new Job() { - @Override - public String getName() { - return "discreteLocalJob2"; - } - - @Override - public void execute(JobExecution execution) { - execution.setStatus(BatchStatus.COMPLETED); - } - }; - } - - } - - @Configuration(proxyBeanMethods = false) - static class JobConfiguration { - - @Autowired - private JobRepository jobRepository; - - @Bean - Job job() { - AbstractJob job = new AbstractJob() { - - @Override - public Collection getStepNames() { - return Collections.emptySet(); - } - - @Override - public Step getStep(String stepName) { - return null; - } - - @Override - protected void doExecute(JobExecution execution) { - execution.setStatus(BatchStatus.COMPLETED); - } - }; - job.setJobRepository(this.jobRepository); - return job; - } - - } - - @Configuration(proxyBeanMethods = false) - static class CustomBatchDatabaseInitializerConfiguration { - - @Bean - BatchDataSourceScriptDatabaseInitializer customInitializer(DataSource dataSource, BatchProperties properties) { - return new BatchDataSourceScriptDatabaseInitializer(dataSource, properties.getJdbc()); - } - - } - - @Configuration(proxyBeanMethods = false) - static class CustomDatabaseInitializerConfiguration { - - @Bean - DataSourceScriptDatabaseInitializer customInitializer(DataSource dataSource) { - return new DataSourceScriptDatabaseInitializer(dataSource, new DatabaseInitializationSettings()); - } - - } - @Configuration(proxyBeanMethods = false) static class CustomBatchConfiguration extends DefaultBatchConfiguration { diff --git a/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchJobLauncherAutoConfigurationTests.java b/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchJobLauncherAutoConfigurationTests.java new file mode 100644 index 00000000000..1ffbab43f4b --- /dev/null +++ b/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/BatchJobLauncherAutoConfigurationTests.java @@ -0,0 +1,277 @@ +/* + * 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.autoconfigure; + +import java.util.Collection; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.job.AbstractJob; +import org.springframework.batch.core.job.Job; +import org.springframework.batch.core.job.JobExecution; +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.Step; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.DefaultApplicationArguments; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link BatchJobLauncherAutoConfiguration}. + * + * @author Stephane Nicoll + */ +class BatchJobLauncherAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( + AutoConfigurations.of(BatchAutoConfiguration.class, BatchJobLauncherAutoConfiguration.class)); + + @Test + void testDefinesAndLaunchesJob() { + this.contextRunner.withUserConfiguration(JobConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(JobOperator.class); + context.getBean(JobLauncherApplicationRunner.class).run(new DefaultApplicationArguments("jobParam=test")); + JobParameters jobParameters = new JobParametersBuilder().addString("jobParam", "test").toJobParameters(); + assertThat(context.getBean(JobRepository.class).getLastJobExecution("job", jobParameters)).isNotNull(); + }); + } + + @Test + void testDefinesAndLaunchesJobIgnoreOptionArguments() { + this.contextRunner.withUserConfiguration(JobConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(JobOperator.class); + context.getBean(JobLauncherApplicationRunner.class) + .run(new DefaultApplicationArguments("--spring.property=value", "jobParam=test")); + JobParameters jobParameters = new JobParametersBuilder().addString("jobParam", "test").toJobParameters(); + assertThat(context.getBean(JobRepository.class).getLastJobExecution("job", jobParameters)).isNotNull(); + }); + } + + @Test + void testRegisteredAndLocalJob() { + this.contextRunner.withUserConfiguration(NamedJobConfigurationWithRegisteredAndLocalJob.class) + .withPropertyValues("spring.batch.job.name:discreteRegisteredJob") + .run((context) -> { + assertThat(context).hasSingleBean(JobOperator.class); + context.getBean(JobLauncherApplicationRunner.class).run(); + assertThat(context.getBean(JobRepository.class) + .getLastJobExecution("discreteRegisteredJob", new JobParameters()) + .getStatus()).isEqualTo(BatchStatus.COMPLETED); + }); + } + + @Test + void testDefinesAndLaunchesLocalJob() { + this.contextRunner.withUserConfiguration(NamedJobConfigurationWithLocalJob.class) + .withPropertyValues("spring.batch.job.name:discreteLocalJob") + .run((context) -> { + assertThat(context).hasSingleBean(JobOperator.class); + context.getBean(JobLauncherApplicationRunner.class).run(); + assertThat(context.getBean(JobRepository.class) + .getLastJobExecution("discreteLocalJob", new JobParameters())).isNotNull(); + }); + } + + @Test + void testMultipleJobsAndNoJobName() { + this.contextRunner.withUserConfiguration(MultipleJobConfiguration.class).run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure().getCause().getMessage()) + .contains("Job name must be specified in case of multiple jobs"); + }); + } + + @Test + void testMultipleJobsAndJobName() { + this.contextRunner.withUserConfiguration(MultipleJobConfiguration.class) + .withPropertyValues("spring.batch.job.name:discreteLocalJob") + .run((context) -> { + assertThat(context).hasSingleBean(JobOperator.class); + context.getBean(JobLauncherApplicationRunner.class).run(); + assertThat(context.getBean(JobRepository.class) + .getLastJobExecution("discreteLocalJob", new JobParameters())).isNotNull(); + }); + } + + @Test + void testDisableLaunchesJob() { + this.contextRunner.withUserConfiguration(JobConfiguration.class) + .withPropertyValues("spring.batch.job.enabled:false") + .run((context) -> { + assertThat(context).hasSingleBean(JobOperator.class); + assertThat(context).doesNotHaveBean(CommandLineRunner.class); + }); + } + + @Configuration(proxyBeanMethods = false) + static class NamedJobConfigurationWithRegisteredAndLocalJob { + + @Autowired + private JobRepository jobRepository; + + @Bean + Job discreteJob() { + AbstractJob job = new AbstractJob("discreteRegisteredJob") { + + private static int count = 0; + + @Override + public Collection getStepNames() { + return Collections.emptySet(); + } + + @Override + public Step getStep(String stepName) { + return null; + } + + @Override + protected void doExecute(JobExecution execution) { + if (count == 0) { + execution.setStatus(BatchStatus.COMPLETED); + } + else { + execution.setStatus(BatchStatus.FAILED); + } + count++; + } + }; + job.setJobRepository(this.jobRepository); + return job; + } + + } + + @Configuration(proxyBeanMethods = false) + static class NamedJobConfigurationWithLocalJob { + + @Autowired + private JobRepository jobRepository; + + @Bean + Job discreteJob() { + AbstractJob job = new AbstractJob("discreteLocalJob") { + + @Override + public Collection getStepNames() { + return Collections.emptySet(); + } + + @Override + public Step getStep(String stepName) { + return null; + } + + @Override + protected void doExecute(JobExecution execution) { + execution.setStatus(BatchStatus.COMPLETED); + } + }; + job.setJobRepository(this.jobRepository); + return job; + } + + } + + @Configuration(proxyBeanMethods = false) + static class MultipleJobConfiguration { + + @Autowired + private JobRepository jobRepository; + + @Bean + Job discreteJob() { + AbstractJob job = new AbstractJob("discreteLocalJob") { + + @Override + public Collection getStepNames() { + return Collections.emptySet(); + } + + @Override + public Step getStep(String stepName) { + return null; + } + + @Override + protected void doExecute(JobExecution execution) { + execution.setStatus(BatchStatus.COMPLETED); + } + }; + job.setJobRepository(this.jobRepository); + return job; + } + + @Bean + Job job2() { + return new Job() { + @Override + public String getName() { + return "discreteLocalJob2"; + } + + @Override + public void execute(JobExecution execution) { + execution.setStatus(BatchStatus.COMPLETED); + } + }; + } + + } + + @Configuration(proxyBeanMethods = false) + static class JobConfiguration { + + @Autowired + private JobRepository jobRepository; + + @Bean + Job job() { + AbstractJob job = new AbstractJob() { + + @Override + public Collection getStepNames() { + return Collections.emptySet(); + } + + @Override + public Step getStep(String stepName) { + return null; + } + + @Override + protected void doExecute(JobExecution execution) { + execution.setStatus(BatchStatus.COMPLETED); + } + }; + job.setJobRepository(this.jobRepository); + return job; + } + + } + +} diff --git a/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/JobLauncherApplicationRunnerTests.java b/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/JobLauncherApplicationRunnerTests.java index b378d83b4ca..1f49c43c16f 100644 --- a/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/JobLauncherApplicationRunnerTests.java +++ b/module/spring-boot-batch/src/test/java/org/springframework/boot/batch/autoconfigure/JobLauncherApplicationRunnerTests.java @@ -16,44 +16,30 @@ package org.springframework.boot.batch.autoconfigure; -import java.util.Arrays; import java.util.List; -import javax.sql.DataSource; - import org.junit.jupiter.api.Test; +import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; -import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; +import org.springframework.batch.core.job.JobExecution; import org.springframework.batch.core.job.JobExecutionException; import org.springframework.batch.core.job.JobInstance; import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.job.builder.SimpleJobBuilder; 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.launch.support.RunIdIncrementer; import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.repository.JobRestartException; import org.springframework.batch.core.step.Step; import org.springframework.batch.core.step.builder.StepBuilder; import org.springframework.batch.core.step.tasklet.Tasklet; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration; -import org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration; -import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; -import org.springframework.boot.sql.init.DatabaseInitializationSettings; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.boot.transaction.autoconfigure.TransactionAutoConfiguration; import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.PlatformTransactionManager; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.fail; /** * Tests for {@link JobLauncherApplicationRunner}. @@ -66,120 +52,37 @@ import static org.assertj.core.api.Assertions.fail; class JobLauncherApplicationRunnerTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, TransactionAutoConfiguration.class, - DataSourceTransactionManagerAutoConfiguration.class)) + .withBean(PlatformTransactionManager.class, ResourcelessTransactionManager::new) .withUserConfiguration(BatchConfiguration.class); @Test - void basicExecution() { + void basicExecutionSuccess() { this.contextRunner.run((context) -> { JobLauncherApplicationRunnerContext jobLauncherContext = new JobLauncherApplicationRunnerContext(context); jobLauncherContext.executeJob(new JobParameters()); - assertThat(jobLauncherContext.jobInstances()).hasSize(1); - jobLauncherContext.executeJob(new JobParametersBuilder().addLong("id", 1L).toJobParameters()); - assertThat(jobLauncherContext.jobInstances()).hasSize(2); + List jobInstances = jobLauncherContext.jobInstances(); + assertThat(jobInstances).hasSize(1); + List jobExecutions = jobLauncherContext.jobExecutions(jobInstances.get(0)); + assertThat(jobExecutions).hasSize(1); + assertThat(jobExecutions.get(0).getExitStatus().getExitCode()) + .isEqualTo(ExitStatus.COMPLETED.getExitCode()); }); } @Test - void incrementExistingExecution() { + void basicExecutionFailure() { this.contextRunner.run((context) -> { JobLauncherApplicationRunnerContext jobLauncherContext = new JobLauncherApplicationRunnerContext(context); - Job job = jobLauncherContext.configureJob().incrementer(new RunIdIncrementer()).build(); - JobParameters jobParameters = new JobParameters(); - jobLauncherContext.runner.execute(job, jobParameters); - jobLauncherContext.runner.execute(job, jobParameters); - assertThat(jobLauncherContext.jobInstances()).hasSize(2); - }); - } - - @Test - void retryFailedExecutionWithIncrementer() { - this.contextRunner.run((context) -> { PlatformTransactionManager transactionManager = context.getBean(PlatformTransactionManager.class); - JobLauncherApplicationRunnerContext jobLauncherContext = new JobLauncherApplicationRunnerContext(context); Job job = jobLauncherContext.jobBuilder() .start(jobLauncherContext.stepBuilder().tasklet(throwingTasklet(), transactionManager).build()) - .incrementer(new RunIdIncrementer()) .build(); jobLauncherContext.runner.execute(job, new JobParameters()); - jobLauncherContext.runner.execute(job, new JobParameters()); - // with an incrementer, we always create a new job instance - assertThat(jobLauncherContext.jobInstances()).hasSize(2); - }); - } - - @Test - void retryFailedExecutionWithoutIncrementer() { - this.contextRunner.run((context) -> { - PlatformTransactionManager transactionManager = context.getBean(PlatformTransactionManager.class); - JobLauncherApplicationRunnerContext jobLauncherContext = new JobLauncherApplicationRunnerContext(context); - Job job = jobLauncherContext.jobBuilder() - .start(jobLauncherContext.stepBuilder().tasklet(throwingTasklet(), transactionManager).build()) - .build(); - JobParameters jobParameters = new JobParametersBuilder().addLong("run.id", 1L).toJobParameters(); - jobLauncherContext.runner.execute(job, jobParameters); - jobLauncherContext.runner.execute(job, jobParameters); - assertThat(jobLauncherContext.jobInstances()).hasSize(1); - }); - } - - @Test - void runDifferentInstances() { - this.contextRunner.run((context) -> { - PlatformTransactionManager transactionManager = context.getBean(PlatformTransactionManager.class); - JobLauncherApplicationRunnerContext jobLauncherContext = new JobLauncherApplicationRunnerContext(context); - Job job = jobLauncherContext.jobBuilder() - .start(jobLauncherContext.stepBuilder().tasklet(throwingTasklet(), transactionManager).build()) - .build(); - // start a job instance - JobParameters jobParameters = new JobParametersBuilder().addString("name", "foo").toJobParameters(); - jobLauncherContext.runner.execute(job, jobParameters); - assertThat(jobLauncherContext.jobInstances()).hasSize(1); - // start a different job instance - JobParameters otherJobParameters = new JobParametersBuilder().addString("name", "bar").toJobParameters(); - jobLauncherContext.runner.execute(job, otherJobParameters); - assertThat(jobLauncherContext.jobInstances()).hasSize(2); - }); - } - - @Test - void retryFailedExecutionOnNonRestartableJob() { - this.contextRunner.run((context) -> { - PlatformTransactionManager transactionManager = context.getBean(PlatformTransactionManager.class); - JobLauncherApplicationRunnerContext jobLauncherContext = new JobLauncherApplicationRunnerContext(context); - Job job = jobLauncherContext.jobBuilder() - .preventRestart() - .start(jobLauncherContext.stepBuilder().tasklet(throwingTasklet(), transactionManager).build()) - .build(); - JobParameters jobParameters = new JobParametersBuilder().addString("name", "foo").toJobParameters(); - jobLauncherContext.runner.execute(job, jobParameters); - assertThat(jobLauncherContext.jobInstances()).hasSize(1); - assertThatExceptionOfType(JobRestartException.class).isThrownBy(() -> { - // try to re-run a failed execution - jobLauncherContext.runner.execute(job, jobParameters); - fail("expected JobRestartException"); - }).withMessageContaining("JobInstance already exists and is not restartable"); - }); - } - - @Test - void retryFailedExecutionWithNonIdentifyingParameters() { - this.contextRunner.run((context) -> { - PlatformTransactionManager transactionManager = context.getBean(PlatformTransactionManager.class); - JobLauncherApplicationRunnerContext jobLauncherContext = new JobLauncherApplicationRunnerContext(context); - Job job = jobLauncherContext.jobBuilder() - .start(jobLauncherContext.stepBuilder().tasklet(throwingTasklet(), transactionManager).build()) - .build(); - JobParameters jobParameters = new JobParametersBuilder().addLong("run.id", 1L, true) - .addLong("foo", 2L, false) - .toJobParameters(); - jobLauncherContext.runner.execute(job, jobParameters); - assertThat(jobLauncherContext.jobInstances()).hasSize(1); - // try to re-run a failed execution with non identifying parameters - jobLauncherContext.runner.execute(job, - new JobParametersBuilder(jobParameters).addLong("run.id", 1L).toJobParameters()); - assertThat(jobLauncherContext.jobInstances()).hasSize(1); + List jobInstances = jobLauncherContext.jobInstances(); + assertThat(jobInstances).hasSize(1); + List jobExecutions = jobLauncherContext.jobExecutions(jobInstances.get(0)); + assertThat(jobExecutions).hasSize(1); + assertThat(jobExecutions.get(0).getExitStatus().getExitCode()).isEqualTo(ExitStatus.FAILED.getExitCode()); }); } @@ -201,16 +104,14 @@ class JobLauncherApplicationRunnerTests { private final StepBuilder stepBuilder; - private final Step step; - JobLauncherApplicationRunnerContext(ApplicationContext context) { JobOperator jobOperator = context.getBean(JobOperator.class); JobRepository jobRepository = context.getBean(JobRepository.class); PlatformTransactionManager transactionManager = context.getBean(PlatformTransactionManager.class); this.stepBuilder = new StepBuilder("step", jobRepository); - this.step = this.stepBuilder.tasklet((contribution, chunkContext) -> null, transactionManager).build(); + Step step = this.stepBuilder.tasklet((contribution, chunkContext) -> null, transactionManager).build(); this.jobBuilder = new JobBuilder("job", jobRepository); - this.job = this.jobBuilder.start(this.step).build(); + this.job = this.jobBuilder.start(step).build(); this.jobRepository = context.getBean(JobRepository.class); this.runner = new JobLauncherApplicationRunner(jobOperator); } @@ -219,6 +120,10 @@ class JobLauncherApplicationRunnerTests { return this.jobRepository.getJobInstances("job", 0, 100); } + List jobExecutions(JobInstance jobInstance) { + return this.jobRepository.getJobExecutions(jobInstance); + } + void executeJob(JobParameters jobParameters) throws JobExecutionException { this.runner.execute(this.job, jobParameters); } @@ -231,30 +136,12 @@ class JobLauncherApplicationRunnerTests { return this.stepBuilder; } - SimpleJobBuilder configureJob() { - return this.jobBuilder.start(this.step); - } - } - @EnableBatchProcessing - @EnableJdbcJobRepository @Configuration(proxyBeanMethods = false) + @EnableBatchProcessing static class BatchConfiguration { - private final DataSource dataSource; - - protected BatchConfiguration(DataSource dataSource) { - this.dataSource = dataSource; - } - - @Bean - DataSourceScriptDatabaseInitializer batchDataSourceInitializer() { - DatabaseInitializationSettings settings = new DatabaseInitializationSettings(); - settings.setSchemaLocations(Arrays.asList("classpath:org/springframework/batch/core/schema-h2.sql")); - return new DataSourceScriptDatabaseInitializer(this.dataSource, settings); - } - } } diff --git a/settings.gradle b/settings.gradle index e34f9c1805c..0d5ac38c128 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-jdbc" include "module:spring-boot-cache" include "module:spring-boot-cache-test" include "module:spring-boot-cassandra" @@ -204,6 +205,7 @@ include "starter:spring-boot-starter-amqp" include "starter:spring-boot-starter-artemis" include "starter:spring-boot-starter-aspectj" include "starter:spring-boot-starter-batch" +include "starter:spring-boot-starter-batch-jdbc" include "starter:spring-boot-starter-cache" include "starter:spring-boot-starter-cassandra" include "starter:spring-boot-starter-classic" @@ -317,6 +319,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-jdbc" include ":smoke-test:spring-boot-smoke-test-bootstrap-registry" include ":smoke-test:spring-boot-smoke-test-cache" include ":smoke-test:spring-boot-smoke-test-config" diff --git a/smoke-test/spring-boot-smoke-test-batch-jdbc/build.gradle b/smoke-test/spring-boot-smoke-test-batch-jdbc/build.gradle new file mode 100644 index 00000000000..b6fa9762cab --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-batch-jdbc/build.gradle @@ -0,0 +1,29 @@ +/* + * 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 JDBC smoke test" + +dependencies { + implementation(project(":starter:spring-boot-starter-batch-jdbc")) + + runtimeOnly("org.hsqldb:hsqldb") + + testImplementation(project(":starter:spring-boot-starter-test")) +} \ No newline at end of file diff --git a/smoke-test/spring-boot-smoke-test-batch-jdbc/src/main/java/smoketest/batch/SampleBatchApplication.java b/smoke-test/spring-boot-smoke-test-batch-jdbc/src/main/java/smoketest/batch/SampleBatchApplication.java new file mode 100644 index 00000000000..db058f3e28b --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-batch-jdbc/src/main/java/smoketest/batch/SampleBatchApplication.java @@ -0,0 +1,55 @@ +/* + * 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.Step; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.transaction.PlatformTransactionManager; + +@SpringBootApplication +public class SampleBatchApplication { + + @Bean + Tasklet tasklet() { + return (contribution, context) -> RepeatStatus.FINISHED; + } + + @Bean + Job job(JobRepository jobRepository, Step step) { + return new JobBuilder("job", jobRepository).start(step).build(); + } + + @Bean + Step step1(JobRepository jobRepository, Tasklet tasklet, PlatformTransactionManager transactionManager) { + return new StepBuilder("step1", jobRepository).tasklet(tasklet, transactionManager).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-jdbc/src/main/java/smoketest/batch/package-info.java b/smoke-test/spring-boot-smoke-test-batch-jdbc/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-jdbc/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-jdbc/src/test/java/smoketest/batch/SampleBatchApplicationTests.java b/smoke-test/spring-boot-smoke-test-batch-jdbc/src/test/java/smoketest/batch/SampleBatchApplicationTests.java new file mode 100644 index 00000000000..d84b677ebb2 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-batch-jdbc/src/test/java/smoketest/batch/SampleBatchApplicationTests.java @@ -0,0 +1,37 @@ +/* + * 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.springframework.boot.SpringApplication; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(OutputCaptureExtension.class) +class SampleBatchApplicationTests { + + @Test + void testDefaultSettings(CapturedOutput output) { + assertThat(SpringApplication.exit(SpringApplication.run(SampleBatchApplication.class))).isZero(); + assertThat(output).contains("completed with the following parameters"); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-batch/build.gradle b/smoke-test/spring-boot-smoke-test-batch/build.gradle index 0989eea536f..e512f23fe05 100644 --- a/smoke-test/spring-boot-smoke-test-batch/build.gradle +++ b/smoke-test/spring-boot-smoke-test-batch/build.gradle @@ -23,7 +23,5 @@ description = "Spring Boot Batch smoke test" dependencies { implementation(project(":starter:spring-boot-starter-batch")) - runtimeOnly("org.hsqldb:hsqldb") - testImplementation(project(":starter:spring-boot-starter-test")) } \ No newline at end of file diff --git a/smoke-test/spring-boot-smoke-test-batch/src/main/java/smoketest/batch/SampleBatchApplication.java b/smoke-test/spring-boot-smoke-test-batch/src/main/java/smoketest/batch/SampleBatchApplication.java index db058f3e28b..c80a143ed7e 100644 --- a/smoke-test/spring-boot-smoke-test-batch/src/main/java/smoketest/batch/SampleBatchApplication.java +++ b/smoke-test/spring-boot-smoke-test-batch/src/main/java/smoketest/batch/SampleBatchApplication.java @@ -26,7 +26,6 @@ import org.springframework.batch.repeat.RepeatStatus; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; -import org.springframework.transaction.PlatformTransactionManager; @SpringBootApplication public class SampleBatchApplication { @@ -42,8 +41,8 @@ public class SampleBatchApplication { } @Bean - Step step1(JobRepository jobRepository, Tasklet tasklet, PlatformTransactionManager transactionManager) { - return new StepBuilder("step1", jobRepository).tasklet(tasklet, transactionManager).build(); + Step step1(JobRepository jobRepository, Tasklet tasklet) { + return new StepBuilder("step1", jobRepository).tasklet(tasklet).build(); } public static void main(String[] args) { diff --git a/starter/spring-boot-starter-batch-jdbc/build.gradle b/starter/spring-boot-starter-batch-jdbc/build.gradle new file mode 100644 index 00000000000..e91829c4537 --- /dev/null +++ b/starter/spring-boot-starter-batch-jdbc/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 JDBC" + +dependencies { + api(project(":starter:spring-boot-starter")) + api(project(":starter:spring-boot-starter-jdbc")) + + api(project(":module:spring-boot-batch-jdbc")) +} diff --git a/starter/spring-boot-starter-batch/build.gradle b/starter/spring-boot-starter-batch/build.gradle index 3d0a7be3060..2c7f7554b2e 100644 --- a/starter/spring-boot-starter-batch/build.gradle +++ b/starter/spring-boot-starter-batch/build.gradle @@ -22,8 +22,6 @@ description = "Starter for using Spring Batch" dependencies { api(project(":starter:spring-boot-starter")) - api(project(":starter:spring-boot-starter-jdbc")) api(project(":module:spring-boot-batch")) - api(project(":module:spring-boot-jdbc")) }