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
This commit is contained in:
Stéphane Nicoll 2025-09-11 16:59:41 +02:00
parent 54ffc42309
commit 4f6bbac13e
39 changed files with 1900 additions and 1089 deletions

View File

@ -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.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-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-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-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-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] * 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/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[#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: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.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[#boot-features-caching-provider-caffeine]
* xref:reference:io/caching.adoc#io.caching.provider.caffeine[#features.caching.provider.caffeine] * xref:reference:io/caching.adoc#io.caching.provider.caffeine[#features.caching.provider.caffeine]

View File

@ -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]] [[howto.batch.running-from-the-command-line]]
== Running From the Command Line == Running From the Command Line

View File

@ -3,6 +3,6 @@
Most applications will need to deal with input and output concerns at some point. 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. 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. We will also cover calling remote REST or SOAP services and sending email.

View File

@ -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.

View File

@ -39,6 +39,7 @@
** xref:reference:io/index.adoc[] ** xref:reference:io/index.adoc[]
*** xref:reference:io/caching.adoc[] *** xref:reference:io/caching.adoc[]
*** xref:reference:io/spring-batch.adoc[]
*** xref:reference:io/hazelcast.adoc[] *** xref:reference:io/hazelcast.adoc[]
*** xref:reference:io/quartz.adoc[] *** xref:reference:io/quartz.adoc[]
*** xref:reference:io/email.adoc[] *** xref:reference:io/email.adoc[]

View File

@ -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")
}

View File

@ -14,7 +14,7 @@
* limitations under the License. * 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.Documented;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.batch.autoconfigure; package org.springframework.boot.batch.jdbc.autoconfigure;
import java.util.List; import java.util.List;
@ -43,7 +43,7 @@ public class BatchDataSourceScriptDatabaseInitializer extends DataSourceScriptDa
* @param properties the Spring Batch JDBC properties * @param properties the Spring Batch JDBC properties
* @see #getSettings * @see #getSettings
*/ */
public BatchDataSourceScriptDatabaseInitializer(DataSource dataSource, BatchProperties.Jdbc properties) { public BatchDataSourceScriptDatabaseInitializer(DataSource dataSource, BatchJdbcProperties properties) {
this(dataSource, getSettings(dataSource, properties)); this(dataSource, getSettings(dataSource, properties));
} }
@ -58,16 +58,15 @@ public class BatchDataSourceScriptDatabaseInitializer extends DataSourceScriptDa
} }
/** /**
* Adapts {@link BatchProperties.Jdbc Spring Batch JDBC properties} to * Adapts {@link BatchJdbcProperties} to {@link DatabaseInitializationSettings}
* {@link DatabaseInitializationSettings} replacing any {@literal @@platform@@} * replacing any {@literal @@platform@@} placeholders.
* placeholders.
* @param dataSource the Spring Batch data source * @param dataSource the Spring Batch data source
* @param properties batch JDBC properties * @param properties batch JDBC properties
* @return a new {@link DatabaseInitializationSettings} instance * @return a new {@link DatabaseInitializationSettings} instance
* @see #BatchDataSourceScriptDatabaseInitializer(DataSource, * @see #BatchDataSourceScriptDatabaseInitializer(DataSource,
* DatabaseInitializationSettings) * DatabaseInitializationSettings)
*/ */
public static DatabaseInitializationSettings getSettings(DataSource dataSource, BatchProperties.Jdbc properties) { public static DatabaseInitializationSettings getSettings(DataSource dataSource, BatchJdbcProperties properties) {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings(); DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(resolveSchemaLocations(dataSource, properties)); settings.setSchemaLocations(resolveSchemaLocations(dataSource, properties));
settings.setMode(properties.getInitializeSchema()); settings.setMode(properties.getInitializeSchema());
@ -75,7 +74,7 @@ public class BatchDataSourceScriptDatabaseInitializer extends DataSourceScriptDa
return settings; return settings;
} }
private static List<String> resolveSchemaLocations(DataSource dataSource, BatchProperties.Jdbc properties) { private static List<String> resolveSchemaLocations(DataSource dataSource, BatchJdbcProperties properties) {
PlatformPlaceholderDatabaseDriverResolver platformResolver = new PlatformPlaceholderDatabaseDriverResolver(); PlatformPlaceholderDatabaseDriverResolver platformResolver = new PlatformPlaceholderDatabaseDriverResolver();
if (StringUtils.hasText(properties.getPlatform())) { if (StringUtils.hasText(properties.getPlatform())) {
return platformResolver.resolveAll(properties.getPlatform(), properties.getSchema()); return platformResolver.resolveAll(properties.getPlatform(), properties.getSchema());

View File

@ -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<BatchConversionServiceCustomizer> batchConversionServiceCustomizers;
private final @Nullable ExecutionContextSerializer executionContextSerializer;
private final @Nullable JobParametersConverter jobParametersConverter;
SpringBootBatchJdbcConfiguration(DataSource dataSource,
@BatchDataSource ObjectProvider<DataSource> batchDataSource,
PlatformTransactionManager transactionManager,
@BatchTransactionManager ObjectProvider<PlatformTransactionManager> batchTransactionManager,
@BatchTaskExecutor ObjectProvider<TaskExecutor> batchTaskExecutor, BatchJdbcProperties properties,
ObjectProvider<BatchConversionServiceCustomizer> batchConversionServiceCustomizers,
ObjectProvider<ExecutionContextSerializer> executionContextSerializer,
ObjectProvider<JobParametersConverter> 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<DataSource> batchDataSource, BatchJdbcProperties properties) {
return new BatchDataSourceScriptDatabaseInitializer(batchDataSource.getIfAvailable(() -> dataSource),
properties);
}
}
static class OnBatchDatasourceInitializationCondition extends OnDatabaseInitializationCondition {
OnBatchDatasourceInitializationCondition() {
super("Batch", "spring.batch.jdbc.initialize-schema");
}
}
}
}

View File

@ -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;
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.batch.autoconfigure; package org.springframework.boot.batch.jdbc.autoconfigure;
import java.util.Collections; import java.util.Collections;
import java.util.Set; import java.util.Set;

View File

@ -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;

View File

@ -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"
}
}
]
}

View File

@ -1,3 +1,3 @@
# Depends on Database Initialization Detectors # Depends on Database Initialization Detectors
org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector=\ org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector=\
org.springframework.boot.batch.autoconfigure.JobRepositoryDependsOnDatabaseInitializationDetector org.springframework.boot.batch.jdbc.autoconfigure.JobRepositoryDependsOnDatabaseInitializationDetector

View File

@ -0,0 +1 @@
org.springframework.boot.batch.jdbc.autoconfigure.BatchJdbcAutoConfiguration

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.batch.autoconfigure; package org.springframework.boot.batch.jdbc.autoconfigure;
import java.io.IOException; import java.io.IOException;
import java.sql.Connection; import java.sql.Connection;
@ -52,10 +52,10 @@ class BatchDataSourceScriptDatabaseInitializerTests {
@Test @Test
void getSettingsWithPlatformDoesNotTouchDataSource() { void getSettingsWithPlatformDoesNotTouchDataSource() {
DataSource dataSource = mock(DataSource.class); DataSource dataSource = mock(DataSource.class);
BatchProperties properties = new BatchProperties(); BatchJdbcProperties properties = new BatchJdbcProperties();
properties.getJdbc().setPlatform("test"); properties.setPlatform("test");
DatabaseInitializationSettings settings = BatchDataSourceScriptDatabaseInitializer.getSettings(dataSource, DatabaseInitializationSettings settings = BatchDataSourceScriptDatabaseInitializer.getSettings(dataSource,
properties.getJdbc()); properties);
assertThat(settings.getSchemaLocations()) assertThat(settings.getSchemaLocations())
.containsOnly("classpath:org/springframework/batch/core/schema-test.sql"); .containsOnly("classpath:org/springframework/batch/core/schema-test.sql");
then(dataSource).shouldHaveNoInteractions(); then(dataSource).shouldHaveNoInteractions();
@ -66,7 +66,7 @@ class BatchDataSourceScriptDatabaseInitializerTests {
"INFORMIX", "JTDS", "PHOENIX", "REDSHIFT", "TERADATA", "TESTCONTAINERS", "UNKNOWN" }) "INFORMIX", "JTDS", "PHOENIX", "REDSHIFT", "TERADATA", "TESTCONTAINERS", "UNKNOWN" })
void batchSchemaCanBeLocated(DatabaseDriver driver) throws SQLException { void batchSchemaCanBeLocated(DatabaseDriver driver) throws SQLException {
DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
BatchProperties properties = new BatchProperties(); BatchJdbcProperties properties = new BatchJdbcProperties();
DataSource dataSource = mock(DataSource.class); DataSource dataSource = mock(DataSource.class);
Connection connection = mock(Connection.class); Connection connection = mock(Connection.class);
given(dataSource.getConnection()).willReturn(connection); given(dataSource.getConnection()).willReturn(connection);
@ -75,7 +75,7 @@ class BatchDataSourceScriptDatabaseInitializerTests {
String productName = (String) ReflectionTestUtils.getField(driver, "productName"); String productName = (String) ReflectionTestUtils.getField(driver, "productName");
given(metadata.getDatabaseProductName()).willReturn(productName); given(metadata.getDatabaseProductName()).willReturn(productName);
DatabaseInitializationSettings settings = BatchDataSourceScriptDatabaseInitializer.getSettings(dataSource, DatabaseInitializationSettings settings = BatchDataSourceScriptDatabaseInitializer.getSettings(dataSource,
properties.getJdbc()); properties);
List<String> schemaLocations = settings.getSchemaLocations(); List<String> schemaLocations = settings.getSchemaLocations();
assertThat(schemaLocations).isNotEmpty() assertThat(schemaLocations).isNotEmpty()
.allSatisfy((location) -> assertThat(resourceLoader.getResource(location).exists()).isTrue()); .allSatisfy((location) -> assertThat(resourceLoader.getResource(location).exists()).isTrue());

View File

@ -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<String> 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<String> 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<String> 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<String> 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);
}
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.batch.autoconfigure; package org.springframework.boot.batch.jdbc.autoconfigure;
import javax.sql.DataSource; 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.batch.core.repository.JobRepository;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage;
import org.springframework.boot.batch.autoconfigure.BatchAutoConfiguration.SpringBootBatchConfiguration; import org.springframework.boot.batch.jdbc.autoconfigure.BatchJdbcAutoConfiguration.SpringBootBatchJdbcConfiguration;
import org.springframework.boot.batch.autoconfigure.domain.City; import org.springframework.boot.batch.jdbc.autoconfigure.domain.City;
import org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration; import org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.jdbc.autoconfigure.EmbeddedDataSourceConfiguration; import org.springframework.boot.jdbc.autoconfigure.EmbeddedDataSourceConfiguration;
import org.springframework.boot.sql.init.DatabaseInitializationMode; 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; 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 * @author Stephane Nicoll
*/ */
@ClassPathExclusions("hibernate-jpa-*.jar") @ClassPathExclusions("hibernate-jpa-*.jar")
class BatchAutoConfigurationWithoutJpaTests { class BatchJdbcAutoConfigurationWithoutJpaTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(BatchAutoConfiguration.class, TransactionAutoConfiguration.class, .withConfiguration(AutoConfigurations.of(BatchJdbcAutoConfiguration.class, TransactionAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class)); DataSourceTransactionManagerAutoConfiguration.class));
@Test @Test
@ -58,7 +58,7 @@ class BatchAutoConfigurationWithoutJpaTests {
.run((context) -> { .run((context) -> {
assertThat(context).hasSingleBean(JobOperator.class); assertThat(context).hasSingleBean(JobOperator.class);
assertThat(context).hasSingleBean(JobRepository.class); assertThat(context).hasSingleBean(JobRepository.class);
assertThat(context.getBean(BatchProperties.class).getJdbc().getInitializeSchema()) assertThat(context.getBean(BatchJdbcProperties.class).getInitializeSchema())
.isEqualTo(DatabaseInitializationMode.EMBEDDED); .isEqualTo(DatabaseInitializationMode.EMBEDDED);
assertThat(new JdbcTemplate(context.getBean(DataSource.class)) assertThat(new JdbcTemplate(context.getBean(DataSource.class))
.queryForList("select * from BATCH_JOB_EXECUTION")).isEmpty(); .queryForList("select * from BATCH_JOB_EXECUTION")).isEmpty();
@ -89,7 +89,7 @@ class BatchAutoConfigurationWithoutJpaTests {
.withPropertyValues("spring.datasource.generate-unique-name=true", .withPropertyValues("spring.datasource.generate-unique-name=true",
"spring.batch.jdbc.isolation-level-for-create=read_committed") "spring.batch.jdbc.isolation-level-for-create=read_committed")
.run((context) -> assertThat( .run((context) -> assertThat(
context.getBean(SpringBootBatchConfiguration.class).getIsolationLevelForCreate()) context.getBean(SpringBootBatchJdbcConfiguration.class).getIsolationLevelForCreate())
.isEqualTo(Isolation.READ_COMMITTED)); .isEqualTo(Isolation.READ_COMMITTED));
} }

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.batch.autoconfigure; package org.springframework.boot.batch.jdbc.autoconfigure;
import org.junit.jupiter.api.Test; 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; import static org.assertj.core.api.Assertions.assertThat;
/** /**
* Tests for {@link BatchProperties}. * Tests for {@link BatchJdbcProperties}.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
class BatchPropertiesTests { class BatchJdbcPropertiesTests {
@Test @Test
void validateTransactionStateDefaultMatchesSpringBatchDefault() { void validateTransactionStateDefaultMatchesSpringBatchDefault() {
assertThat(new BatchProperties().getJdbc().isValidateTransactionState()) assertThat(new BatchJdbcProperties().isValidateTransactionState())
.isEqualTo(new TestBatchConfiguration().getValidateTransactionState()); .isEqualTo(new TestBatchConfiguration().getValidateTransactionState());
} }

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.batch.autoconfigure.domain; package org.springframework.boot.batch.jdbc.autoconfigure.domain;
import java.io.Serializable; import java.io.Serializable;

View File

@ -26,18 +26,12 @@ description = "Spring Boot Batch"
dependencies { dependencies {
api(project(":core:spring-boot")) api(project(":core:spring-boot"))
api(project(":module:spring-boot-jdbc"))
api("org.springframework.batch:spring-batch-core") api("org.springframework.batch:spring-batch-core")
implementation(project(":module:spring-boot-tx"))
optional(project(":core:spring-boot-autoconfigure")) optional(project(":core:spring-boot-autoconfigure"))
optional(project(":module:spring-boot-hibernate"))
optional(project(":module:spring-boot-micrometer-observation")) optional(project(":module:spring-boot-micrometer-observation"))
testImplementation(project(":core:spring-boot-test")) 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(project(":test-support:spring-boot-test-support"))
testImplementation(testFixtures(project(":core:spring-boot-autoconfigure"))) testImplementation(testFixtures(project(":core:spring-boot-autoconfigure")))
testImplementation("io.micrometer:micrometer-observation-test") testImplementation("io.micrometer:micrometer-observation-test")
@ -45,6 +39,4 @@ dependencies {
testRuntimeOnly("ch.qos.logback:logback-classic") testRuntimeOnly("ch.qos.logback:logback-classic")
testRuntimeOnly("com.fasterxml.jackson.core:jackson-databind") testRuntimeOnly("com.fasterxml.jackson.core:jackson-databind")
testRuntimeOnly("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") testRuntimeOnly("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
testRuntimeOnly("com.h2database:h2")
testRuntimeOnly("com.zaxxer:HikariCP")
} }

View File

@ -16,163 +16,47 @@
package org.springframework.boot.batch.autoconfigure; package org.springframework.boot.batch.autoconfigure;
import java.util.List;
import javax.sql.DataSource;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration; 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.converter.JobParametersConverter;
import org.springframework.batch.core.launch.JobOperator; import org.springframework.batch.core.launch.JobOperator;
import org.springframework.batch.core.repository.ExecutionContextSerializer;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.ExitCodeGenerator;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 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.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties; 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.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.core.task.TaskExecutor; 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 * {@link EnableAutoConfiguration Auto-configuration} for Spring Batch using an in-memory
* found in the context, it will be executed on startup. * store.
* <p>
* Disable this behavior with {@literal spring.batch.job.enabled=false}).
* <p>
* 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 Stephane Nicoll
* @author Eddú Meléndez
* @author Kazuki Shimizu
* @author Mahmoud Ben Hassine
* @author Lars Uffmann
* @author Lasse Wulff
* @author Yanming Zhou
* @since 4.0.0 * @since 4.0.0
*/ */
@AutoConfiguration(after = { DataSourceAutoConfiguration.class, TransactionAutoConfiguration.class }, @AutoConfiguration
afterName = "org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration") @ConditionalOnClass(JobOperator.class)
@ConditionalOnClass({ JobOperator.class, DataSource.class, DatabasePopulator.class })
@ConditionalOnBean({ DataSource.class, PlatformTransactionManager.class })
@ConditionalOnMissingBean(value = DefaultBatchConfiguration.class, annotation = EnableBatchProcessing.class) @ConditionalOnMissingBean(value = DefaultBatchConfiguration.class, annotation = EnableBatchProcessing.class)
@EnableConfigurationProperties(BatchProperties.class) @EnableConfigurationProperties(BatchProperties.class)
@Import(DatabaseInitializationDependencyConfigurer.class)
public final class BatchAutoConfiguration { 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) @Configuration(proxyBeanMethods = false)
static class SpringBootBatchConfiguration extends JdbcDefaultBatchConfiguration { static class SpringBootBatchDefaultConfiguration extends DefaultBatchConfiguration {
private final DataSource dataSource;
private final PlatformTransactionManager transactionManager;
private final @Nullable TaskExecutor taskExecutor; private final @Nullable TaskExecutor taskExecutor;
private final BatchProperties properties;
private final List<BatchConversionServiceCustomizer> batchConversionServiceCustomizers;
private final @Nullable ExecutionContextSerializer executionContextSerializer;
private final @Nullable JobParametersConverter jobParametersConverter; private final @Nullable JobParametersConverter jobParametersConverter;
SpringBootBatchConfiguration(DataSource dataSource, @BatchDataSource ObjectProvider<DataSource> batchDataSource, SpringBootBatchDefaultConfiguration(@BatchTaskExecutor ObjectProvider<TaskExecutor> batchTaskExecutor,
PlatformTransactionManager transactionManager,
@BatchTransactionManager ObjectProvider<PlatformTransactionManager> batchTransactionManager,
@BatchTaskExecutor ObjectProvider<TaskExecutor> batchTaskExecutor, BatchProperties properties,
ObjectProvider<BatchConversionServiceCustomizer> batchConversionServiceCustomizers,
ObjectProvider<ExecutionContextSerializer> executionContextSerializer,
ObjectProvider<JobParametersConverter> jobParametersConverter) { ObjectProvider<JobParametersConverter> jobParametersConverter) {
this.dataSource = batchDataSource.getIfAvailable(() -> dataSource);
this.transactionManager = batchTransactionManager.getIfAvailable(() -> transactionManager);
this.taskExecutor = batchTaskExecutor.getIfAvailable(); this.taskExecutor = batchTaskExecutor.getIfAvailable();
this.properties = properties;
this.batchConversionServiceCustomizers = batchConversionServiceCustomizers.orderedStream().toList();
this.executionContextSerializer = executionContextSerializer.getIfAvailable();
this.jobParametersConverter = jobParametersConverter.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 @Override
@Deprecated(since = "4.0.0", forRemoval = true) @Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal") @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<DataSource> batchDataSource, BatchProperties properties) {
return new BatchDataSourceScriptDatabaseInitializer(batchDataSource.getIfAvailable(() -> dataSource),
properties.getJdbc());
}
}
static class OnBatchDatasourceInitializationCondition extends OnDatabaseInitializationCondition {
OnBatchDatasourceInitializationCondition() {
super("Batch", "spring.batch.jdbc.initialize-schema");
}
}
} }

View File

@ -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.
* <p>
* Disable this behavior with {@literal spring.batch.job.enabled=false}).
* <p>
* 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();
}
}

View File

@ -16,11 +16,7 @@
package org.springframework.boot.batch.autoconfigure; package org.springframework.boot.batch.autoconfigure;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.context.properties.ConfigurationProperties; 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. * Configuration properties for Spring Batch.
@ -37,16 +33,10 @@ public class BatchProperties {
private final Job job = new Job(); private final Job job = new Job();
private final Jdbc jdbc = new Jdbc();
public Job getJob() { public Job getJob() {
return this.job; return this.job;
} }
public Jdbc getJdbc() {
return this.jdbc;
}
public static class Job { 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;
}
}
} }

View File

@ -1,43 +1,10 @@
{ {
"properties": [ "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", "name": "spring.batch.job.enabled",
"type": "java.lang.Boolean", "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.", "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 "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"
}
} }
] ]
} }

View File

@ -1,2 +1,3 @@
org.springframework.boot.batch.autoconfigure.BatchAutoConfiguration org.springframework.boot.batch.autoconfigure.BatchAutoConfiguration
org.springframework.boot.batch.autoconfigure.BatchJobLauncherAutoConfiguration
org.springframework.boot.batch.autoconfigure.observation.BatchObservationAutoConfiguration org.springframework.boot.batch.autoconfigure.observation.BatchObservationAutoConfiguration

View File

@ -17,86 +17,42 @@
package org.springframework.boot.batch.autoconfigure; package org.springframework.boot.batch.autoconfigure;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import javax.sql.DataSource;
import jakarta.persistence.EntityManagerFactory;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; 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.JobRegistry;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration; import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration;
import org.springframework.batch.core.converter.DefaultJobParametersConverter; import org.springframework.batch.core.converter.DefaultJobParametersConverter;
import org.springframework.batch.core.converter.JobParametersConverter; import org.springframework.batch.core.converter.JobParametersConverter;
import org.springframework.batch.core.converter.JsonJobParametersConverter; 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.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.launch.JobOperator;
import org.springframework.batch.core.repository.ExecutionContextSerializer;
import org.springframework.batch.core.repository.JobRepository; 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.AutoConfigurations;
import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; import org.springframework.boot.batch.autoconfigure.BatchAutoConfiguration.SpringBootBatchDefaultConfiguration;
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.test.context.FilteredClassLoader; import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.system.OutputCaptureExtension; 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.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.SyncTaskExecutor; import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.core.task.TaskExecutor; 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.jdbc.datasource.init.DatabasePopulator;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.test.util.AopTestUtils; 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.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
/** /**
* Tests for {@link BatchAutoConfiguration}. * Tests for {@link BatchJobLauncherAutoConfiguration}.
* *
* @author Dave Syer * @author Dave Syer
* @author Stephane Nicoll * @author Stephane Nicoll
@ -110,336 +66,43 @@ import static org.mockito.Mockito.mock;
@ExtendWith(OutputCaptureExtension.class) @ExtendWith(OutputCaptureExtension.class)
class BatchAutoConfigurationTests { class BatchAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
AutoConfigurations.of(BatchAutoConfiguration.class, TransactionManagerCustomizationAutoConfiguration.class, .withConfiguration(AutoConfigurations.of(BatchAutoConfiguration.class));
TransactionAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class));
@Test @Test
void testDefaultContext() { void testDefaultContext() {
this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class).run((context) -> { this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(JobRepository.class); assertThat(context).hasSingleBean(JobRepository.class);
assertThat(context).hasSingleBean(JobOperator.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 @Test
void autoConfigurationBacksOffWhenUserEnablesBatchProcessing() { void autoConfigurationBacksOffWhenUserEnablesBatchProcessing() {
this.contextRunner this.contextRunner.withUserConfiguration(EnableBatchProcessingConfiguration.class)
.withUserConfiguration(EnableBatchProcessingConfiguration.class, EmbeddedDataSourceConfiguration.class) .run((context) -> assertThat(context).doesNotHaveBean(SpringBootBatchDefaultConfiguration.class));
.withClassLoader(new FilteredClassLoader(DatabasePopulator.class))
.run((context) -> assertThat(context).doesNotHaveBean(SpringBootBatchConfiguration.class));
} }
@Test @Test
void autoConfigurationBacksOffWhenUserProvidesBatchConfiguration() { void autoConfigurationBacksOffWhenUserProvidesBatchConfiguration() {
this.contextRunner.withUserConfiguration(CustomBatchConfiguration.class, EmbeddedDataSourceConfiguration.class) this.contextRunner.withUserConfiguration(CustomBatchConfiguration.class)
.withClassLoader(new FilteredClassLoader(DatabasePopulator.class)) .withClassLoader(new FilteredClassLoader(DatabasePopulator.class))
.run((context) -> assertThat(context).doesNotHaveBean(SpringBootBatchConfiguration.class)); .run((context) -> assertThat(context).doesNotHaveBean(SpringBootBatchDefaultConfiguration.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);
});
} }
@Test @Test
void testBatchTaskExecutor() { void testBatchTaskExecutor() {
this.contextRunner this.contextRunner.withUserConfiguration(BatchTaskExecutorConfiguration.class).run((context) -> {
.withUserConfiguration(BatchTaskExecutorConfiguration.class, EmbeddedDataSourceConfiguration.class) assertThat(context).hasSingleBean(SpringBootBatchDefaultConfiguration.class).hasBean("batchTaskExecutor");
.run((context) -> { TaskExecutor batchTaskExecutor = context.getBean("batchTaskExecutor", TaskExecutor.class);
assertThat(context).hasSingleBean(SpringBootBatchConfiguration.class).hasBean("batchTaskExecutor"); assertThat(batchTaskExecutor).isInstanceOf(AsyncTaskExecutor.class);
TaskExecutor batchTaskExecutor = context.getBean("batchTaskExecutor", TaskExecutor.class); assertThat(context.getBean(SpringBootBatchDefaultConfiguration.class).getTaskExecutor())
assertThat(batchTaskExecutor).isInstanceOf(AsyncTaskExecutor.class); .isEqualTo(batchTaskExecutor);
assertThat(context.getBean(SpringBootBatchConfiguration.class).getTaskExecutor()) JobOperator jobOperator = AopTestUtils.getTargetObject(context.getBean(JobOperator.class));
.isEqualTo(batchTaskExecutor); assertThat(jobOperator).hasFieldOrPropertyWithValue("taskExecutor", 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(SpringBootBatchConfiguration.class)
.getConversionService();
inOrder.verify(customizer).customize(configurableConversionService);
inOrder.verify(anotherCustomizer).customize(configurableConversionService);
});
}
@Test @Test
void whenTheUserDefinesAJobNameAsJobInstanceValidates() { void whenTheUserDefinesAJobNameAsJobInstanceValidates() {
JobLauncherApplicationRunner runner = createInstance("another"); JobLauncherApplicationRunner runner = createInstance("another");
@ -472,60 +135,24 @@ class BatchAutoConfigurationTests {
.withMessage("No job found with name 'three'"); .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 @Test
@Deprecated(since = "4.0.0", forRemoval = true) @Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal") @SuppressWarnings("removal")
void customJobParametersConverterIsUsed() { void customJobParametersConverterIsUsed() {
this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) this.contextRunner.withBean(JobParametersConverter.class, JsonJobParametersConverter::new).run((context) -> {
.withBean(JobParametersConverter.class, JsonJobParametersConverter::new) assertThat(context).hasSingleBean(JsonJobParametersConverter.class);
.withPropertyValues("spring.datasource.generate-unique-name=true") assertThat(context.getBean(SpringBootBatchDefaultConfiguration.class).getJobParametersConverter())
.run((context) -> { .isInstanceOf(JsonJobParametersConverter.class);
assertThat(context).hasSingleBean(JsonJobParametersConverter.class); });
assertThat(context.getBean(SpringBootBatchConfiguration.class).getJobParametersConverter())
.isInstanceOf(JsonJobParametersConverter.class);
});
} }
@Test @Test
@Deprecated(since = "4.0.0", forRemoval = true) @Deprecated(since = "4.0.0", forRemoval = true)
@SuppressWarnings("removal") @SuppressWarnings("removal")
void defaultJobParametersConverterIsUsed() { void defaultJobParametersConverterIsUsed() {
this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class).run((context) -> { this.contextRunner.run((context) -> {
assertThat(context).doesNotHaveBean(JobParametersConverter.class); assertThat(context).doesNotHaveBean(JobParametersConverter.class);
assertThat(context.getBean(SpringBootBatchConfiguration.class).getJobParametersConverter()) assertThat(context.getBean(SpringBootBatchDefaultConfiguration.class).getJobParametersConverter())
.isInstanceOf(DefaultJobParametersConverter.class); .isInstanceOf(DefaultJobParametersConverter.class);
}); });
} }
@ -544,44 +171,6 @@ class BatchAutoConfigurationTests {
return job; 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) @Configuration(proxyBeanMethods = false)
static class BatchTaskExecutorConfiguration { 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<String> 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<String> 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<String> 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<String> 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) @Configuration(proxyBeanMethods = false)
static class CustomBatchConfiguration extends DefaultBatchConfiguration { static class CustomBatchConfiguration extends DefaultBatchConfiguration {

View File

@ -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<String> 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<String> 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<String> 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<String> 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;
}
}
}

View File

@ -16,44 +16,30 @@
package org.springframework.boot.batch.autoconfigure; package org.springframework.boot.batch.autoconfigure;
import java.util.Arrays;
import java.util.List; import java.util.List;
import javax.sql.DataSource;
import org.junit.jupiter.api.Test; 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.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository;
import org.springframework.batch.core.job.Job; 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.JobExecutionException;
import org.springframework.batch.core.job.JobInstance; import org.springframework.batch.core.job.JobInstance;
import org.springframework.batch.core.job.builder.JobBuilder; 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.JobParameters;
import org.springframework.batch.core.job.parameters.JobParametersBuilder;
import org.springframework.batch.core.launch.JobOperator; 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.JobRepository;
import org.springframework.batch.core.repository.JobRestartException;
import org.springframework.batch.core.step.Step; import org.springframework.batch.core.step.Step;
import org.springframework.batch.core.step.builder.StepBuilder; import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.batch.support.transaction.ResourcelessTransactionManager;
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.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.transaction.autoconfigure.TransactionAutoConfiguration;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import static org.assertj.core.api.Assertions.assertThat; 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}. * Tests for {@link JobLauncherApplicationRunner}.
@ -66,120 +52,37 @@ import static org.assertj.core.api.Assertions.fail;
class JobLauncherApplicationRunnerTests { class JobLauncherApplicationRunnerTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, TransactionAutoConfiguration.class, .withBean(PlatformTransactionManager.class, ResourcelessTransactionManager::new)
DataSourceTransactionManagerAutoConfiguration.class))
.withUserConfiguration(BatchConfiguration.class); .withUserConfiguration(BatchConfiguration.class);
@Test @Test
void basicExecution() { void basicExecutionSuccess() {
this.contextRunner.run((context) -> { this.contextRunner.run((context) -> {
JobLauncherApplicationRunnerContext jobLauncherContext = new JobLauncherApplicationRunnerContext(context); JobLauncherApplicationRunnerContext jobLauncherContext = new JobLauncherApplicationRunnerContext(context);
jobLauncherContext.executeJob(new JobParameters()); jobLauncherContext.executeJob(new JobParameters());
assertThat(jobLauncherContext.jobInstances()).hasSize(1); List<JobInstance> jobInstances = jobLauncherContext.jobInstances();
jobLauncherContext.executeJob(new JobParametersBuilder().addLong("id", 1L).toJobParameters()); assertThat(jobInstances).hasSize(1);
assertThat(jobLauncherContext.jobInstances()).hasSize(2); List<JobExecution> jobExecutions = jobLauncherContext.jobExecutions(jobInstances.get(0));
assertThat(jobExecutions).hasSize(1);
assertThat(jobExecutions.get(0).getExitStatus().getExitCode())
.isEqualTo(ExitStatus.COMPLETED.getExitCode());
}); });
} }
@Test @Test
void incrementExistingExecution() { void basicExecutionFailure() {
this.contextRunner.run((context) -> { this.contextRunner.run((context) -> {
JobLauncherApplicationRunnerContext jobLauncherContext = new JobLauncherApplicationRunnerContext(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); PlatformTransactionManager transactionManager = context.getBean(PlatformTransactionManager.class);
JobLauncherApplicationRunnerContext jobLauncherContext = new JobLauncherApplicationRunnerContext(context);
Job job = jobLauncherContext.jobBuilder() Job job = jobLauncherContext.jobBuilder()
.start(jobLauncherContext.stepBuilder().tasklet(throwingTasklet(), transactionManager).build()) .start(jobLauncherContext.stepBuilder().tasklet(throwingTasklet(), transactionManager).build())
.incrementer(new RunIdIncrementer())
.build(); .build();
jobLauncherContext.runner.execute(job, new JobParameters()); jobLauncherContext.runner.execute(job, new JobParameters());
jobLauncherContext.runner.execute(job, new JobParameters()); List<JobInstance> jobInstances = jobLauncherContext.jobInstances();
// with an incrementer, we always create a new job instance assertThat(jobInstances).hasSize(1);
assertThat(jobLauncherContext.jobInstances()).hasSize(2); List<JobExecution> jobExecutions = jobLauncherContext.jobExecutions(jobInstances.get(0));
}); assertThat(jobExecutions).hasSize(1);
} assertThat(jobExecutions.get(0).getExitStatus().getExitCode()).isEqualTo(ExitStatus.FAILED.getExitCode());
@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);
}); });
} }
@ -201,16 +104,14 @@ class JobLauncherApplicationRunnerTests {
private final StepBuilder stepBuilder; private final StepBuilder stepBuilder;
private final Step step;
JobLauncherApplicationRunnerContext(ApplicationContext context) { JobLauncherApplicationRunnerContext(ApplicationContext context) {
JobOperator jobOperator = context.getBean(JobOperator.class); JobOperator jobOperator = context.getBean(JobOperator.class);
JobRepository jobRepository = context.getBean(JobRepository.class); JobRepository jobRepository = context.getBean(JobRepository.class);
PlatformTransactionManager transactionManager = context.getBean(PlatformTransactionManager.class); PlatformTransactionManager transactionManager = context.getBean(PlatformTransactionManager.class);
this.stepBuilder = new StepBuilder("step", jobRepository); 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.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.jobRepository = context.getBean(JobRepository.class);
this.runner = new JobLauncherApplicationRunner(jobOperator); this.runner = new JobLauncherApplicationRunner(jobOperator);
} }
@ -219,6 +120,10 @@ class JobLauncherApplicationRunnerTests {
return this.jobRepository.getJobInstances("job", 0, 100); return this.jobRepository.getJobInstances("job", 0, 100);
} }
List<JobExecution> jobExecutions(JobInstance jobInstance) {
return this.jobRepository.getJobExecutions(jobInstance);
}
void executeJob(JobParameters jobParameters) throws JobExecutionException { void executeJob(JobParameters jobParameters) throws JobExecutionException {
this.runner.execute(this.job, jobParameters); this.runner.execute(this.job, jobParameters);
} }
@ -231,30 +136,12 @@ class JobLauncherApplicationRunnerTests {
return this.stepBuilder; return this.stepBuilder;
} }
SimpleJobBuilder configureJob() {
return this.jobBuilder.start(this.step);
}
} }
@EnableBatchProcessing
@EnableJdbcJobRepository
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@EnableBatchProcessing
static class BatchConfiguration { 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);
}
} }
} }

View File

@ -85,6 +85,7 @@ include "module:spring-boot-artemis"
include "module:spring-boot-autoconfigure-classic" include "module:spring-boot-autoconfigure-classic"
include "module:spring-boot-autoconfigure-classic-modules" include "module:spring-boot-autoconfigure-classic-modules"
include "module:spring-boot-batch" include "module:spring-boot-batch"
include "module:spring-boot-batch-jdbc"
include "module:spring-boot-cache" include "module:spring-boot-cache"
include "module:spring-boot-cache-test" include "module:spring-boot-cache-test"
include "module:spring-boot-cassandra" 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-artemis"
include "starter:spring-boot-starter-aspectj" include "starter:spring-boot-starter-aspectj"
include "starter:spring-boot-starter-batch" include "starter:spring-boot-starter-batch"
include "starter:spring-boot-starter-batch-jdbc"
include "starter:spring-boot-starter-cache" include "starter:spring-boot-starter-cache"
include "starter:spring-boot-starter-cassandra" include "starter:spring-boot-starter-cassandra"
include "starter:spring-boot-starter-classic" 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-aspectj"
include ":smoke-test:spring-boot-smoke-test-autoconfigure-classic" 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"
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-bootstrap-registry"
include ":smoke-test:spring-boot-smoke-test-cache" include ":smoke-test:spring-boot-smoke-test-cache"
include ":smoke-test:spring-boot-smoke-test-config" include ":smoke-test:spring-boot-smoke-test-config"

View File

@ -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"))
}

View File

@ -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)));
}
}

View File

@ -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;

View File

@ -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");
}
}

View File

@ -23,7 +23,5 @@ description = "Spring Boot Batch smoke test"
dependencies { dependencies {
implementation(project(":starter:spring-boot-starter-batch")) implementation(project(":starter:spring-boot-starter-batch"))
runtimeOnly("org.hsqldb:hsqldb")
testImplementation(project(":starter:spring-boot-starter-test")) testImplementation(project(":starter:spring-boot-starter-test"))
} }

View File

@ -26,7 +26,6 @@ import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.transaction.PlatformTransactionManager;
@SpringBootApplication @SpringBootApplication
public class SampleBatchApplication { public class SampleBatchApplication {
@ -42,8 +41,8 @@ public class SampleBatchApplication {
} }
@Bean @Bean
Step step1(JobRepository jobRepository, Tasklet tasklet, PlatformTransactionManager transactionManager) { Step step1(JobRepository jobRepository, Tasklet tasklet) {
return new StepBuilder("step1", jobRepository).tasklet(tasklet, transactionManager).build(); return new StepBuilder("step1", jobRepository).tasklet(tasklet).build();
} }
public static void main(String[] args) { public static void main(String[] args) {

View File

@ -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"))
}

View File

@ -22,8 +22,6 @@ description = "Starter for using Spring Batch"
dependencies { dependencies {
api(project(":starter:spring-boot-starter")) api(project(":starter:spring-boot-starter"))
api(project(":starter:spring-boot-starter-jdbc"))
api(project(":module:spring-boot-batch")) api(project(":module:spring-boot-batch"))
api(project(":module:spring-boot-jdbc"))
} }