Create spring-boot-batch module

This commit is contained in:
Andy Wilkinson
2025-03-21 16:54:02 +00:00
committed by Phillip Webb
parent 8d5c834df9
commit 06173670b3
32 changed files with 130 additions and 90 deletions

View File

@@ -0,0 +1,210 @@
/*
* Copyright 2012-2025 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.List;
import javax.sql.DataSource;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration;
import org.springframework.batch.core.converter.JobParametersConverter;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.repository.ExecutionContextSerializer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.ExitCodeGenerator;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.sql.init.OnDatabaseInitializationCondition;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer;
import org.springframework.boot.transaction.autoconfigure.TransactionAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.core.task.TaskExecutor;
import org.springframework.jdbc.datasource.init.DatabasePopulator;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.util.StringUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Spring Batch. If a single job is
* found in the context, it will be executed on startup.
* <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 = TransactionAutoConfiguration.class,
afterName = "org.springframework.boot.jpa.autoconfigure.hibernate.HibernateJpaAutoConfiguration")
@ConditionalOnClass({ JobLauncher.class, DataSource.class, DatabasePopulator.class })
@ConditionalOnBean({ DataSource.class, PlatformTransactionManager.class })
@ConditionalOnMissingBean(value = DefaultBatchConfiguration.class, annotation = EnableBatchProcessing.class)
@EnableConfigurationProperties(BatchProperties.class)
@Import(DatabaseInitializationDependencyConfigurer.class)
public class BatchAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnBooleanProperty(name = "spring.batch.job.enabled", matchIfMissing = true)
public JobLauncherApplicationRunner jobLauncherApplicationRunner(JobLauncher jobLauncher, JobExplorer jobExplorer,
JobRepository jobRepository, BatchProperties properties) {
JobLauncherApplicationRunner runner = new JobLauncherApplicationRunner(jobLauncher, jobExplorer, jobRepository);
String jobName = properties.getJob().getName();
if (StringUtils.hasText(jobName)) {
runner.setJobName(jobName);
}
return runner;
}
@Bean
@ConditionalOnMissingBean(ExitCodeGenerator.class)
public JobExecutionExitCodeGenerator jobExecutionExitCodeGenerator() {
return new JobExecutionExitCodeGenerator();
}
@Configuration(proxyBeanMethods = false)
static class SpringBootBatchConfiguration extends DefaultBatchConfiguration {
private final DataSource dataSource;
private final PlatformTransactionManager transactionManager;
private final TaskExecutor taskExecutor;
private final BatchProperties properties;
private final List<BatchConversionServiceCustomizer> batchConversionServiceCustomizers;
private final ExecutionContextSerializer executionContextSerializer;
private final JobParametersConverter jobParametersConverter;
SpringBootBatchConfiguration(DataSource dataSource, @BatchDataSource ObjectProvider<DataSource> batchDataSource,
PlatformTransactionManager transactionManager,
@BatchTransactionManager ObjectProvider<PlatformTransactionManager> batchTransactionManager,
@BatchTaskExecutor ObjectProvider<TaskExecutor> batchTaskExecutor, BatchProperties 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.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
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, 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,40 @@
/*
* Copyright 2012-2025 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.configuration.support.DefaultBatchConfiguration;
import org.springframework.core.convert.support.ConfigurableConversionService;
/**
* Callback interface that can be implemented by beans wishing to customize the
* {@link ConfigurableConversionService} that is
* {@link DefaultBatchConfiguration#getConversionService provided by
* DefaultBatchConfiguration} while retaining its default auto-configuration.
*
* @author Claudio Nave
* @since 4.0.0
*/
@FunctionalInterface
public interface BatchConversionServiceCustomizer {
/**
* Customize the {@link ConfigurableConversionService}.
* @param configurableConversionService the ConfigurableConversionService to customize
*/
void customize(ConfigurableConversionService configurableConversionService);
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2012-2025 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.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
/**
* Qualifier annotation for a DataSource to be injected into Batch auto-configuration. Can
* be used on a secondary data source, if there is another one marked as
* {@link Primary @Primary}.
*
* @author Dmytro Nosan
* @since 4.0.0
*/
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier
public @interface BatchDataSource {
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2012-2025 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.List;
import javax.sql.DataSource;
import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
import org.springframework.boot.jdbc.init.PlatformPlaceholderDatabaseDriverResolver;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.util.StringUtils;
/**
* {@link DataSourceScriptDatabaseInitializer} for the Spring Batch database. May be
* registered as a bean to override auto-configuration.
*
* @author Dave Syer
* @author Vedran Pavic
* @author Andy Wilkinson
* @author Phillip Webb
* @since 4.0.0
*/
public class BatchDataSourceScriptDatabaseInitializer extends DataSourceScriptDatabaseInitializer {
/**
* Create a new {@link BatchDataSourceScriptDatabaseInitializer} instance.
* @param dataSource the Spring Batch data source
* @param properties the Spring Batch JDBC properties
* @see #getSettings
*/
public BatchDataSourceScriptDatabaseInitializer(DataSource dataSource, BatchProperties.Jdbc properties) {
this(dataSource, getSettings(dataSource, properties));
}
/**
* Create a new {@link BatchDataSourceScriptDatabaseInitializer} instance.
* @param dataSource the Spring Batch data source
* @param settings the database initialization settings
* @see #getSettings
*/
public BatchDataSourceScriptDatabaseInitializer(DataSource dataSource, DatabaseInitializationSettings settings) {
super(dataSource, settings);
}
/**
* Adapts {@link BatchProperties.Jdbc Spring Batch JDBC properties} to
* {@link DatabaseInitializationSettings} replacing any {@literal @@platform@@}
* placeholders.
* @param dataSource the Spring Batch data source
* @param properties batch JDBC properties
* @return a new {@link DatabaseInitializationSettings} instance
* @see #BatchDataSourceScriptDatabaseInitializer(DataSource,
* DatabaseInitializationSettings)
*/
public static DatabaseInitializationSettings getSettings(DataSource dataSource, BatchProperties.Jdbc properties) {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(resolveSchemaLocations(dataSource, properties));
settings.setMode(properties.getInitializeSchema());
settings.setContinueOnError(true);
return settings;
}
private static List<String> resolveSchemaLocations(DataSource dataSource, BatchProperties.Jdbc properties) {
PlatformPlaceholderDatabaseDriverResolver platformResolver = new PlatformPlaceholderDatabaseDriverResolver();
if (StringUtils.hasText(properties.getPlatform())) {
return platformResolver.resolveAll(properties.getPlatform(), properties.getSchema());
}
return platformResolver.resolveAll(dataSource, properties.getSchema());
}
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright 2012-2025 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.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.sql.init.DatabaseInitializationMode;
import org.springframework.transaction.annotation.Isolation;
/**
* Configuration properties for Spring Batch.
*
* @author Stephane Nicoll
* @author Eddú Meléndez
* @author Vedran Pavic
* @author Mukul Kumar Chaundhyan
* @author Yanming Zhou
* @since 4.0.0
*/
@ConfigurationProperties("spring.batch")
public class BatchProperties {
private final Job job = new Job();
private final Jdbc jdbc = new Jdbc();
public Job getJob() {
return this.job;
}
public Jdbc getJdbc() {
return this.jdbc;
}
public static class Job {
/**
* Job name to execute on startup. Must be specified if multiple Jobs are found in
* the context.
*/
private String name = "";
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
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 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 String platform;
/**
* Table prefix for all the batch meta-data tables.
*/
private 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 Isolation getIsolationLevelForCreate() {
return this.isolationLevelForCreate;
}
public void setIsolationLevelForCreate(Isolation isolationLevelForCreate) {
this.isolationLevelForCreate = isolationLevelForCreate;
}
public String getSchema() {
return this.schema;
}
public void setSchema(String schema) {
this.schema = schema;
}
public String getPlatform() {
return this.platform;
}
public void setPlatform(String platform) {
this.platform = platform;
}
public String getTablePrefix() {
return this.tablePrefix;
}
public void setTablePrefix(String tablePrefix) {
this.tablePrefix = tablePrefix;
}
public DatabaseInitializationMode getInitializeSchema() {
return this.initializeSchema;
}
public void setInitializeSchema(DatabaseInitializationMode initializeSchema) {
this.initializeSchema = initializeSchema;
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2012-2025 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.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.core.task.TaskExecutor;
/**
* Qualifier annotation for a {@link TaskExecutor} to be injected into Batch
* auto-configuration. Can be used on a secondary task executor source, if there is
* another one marked as {@link Primary @Primary}.
*
* @author Andy Wilkinson
* @since 4.0.0
*/
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier
public @interface BatchTaskExecutor {
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2012-2025 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.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.transaction.PlatformTransactionManager;
/**
* Qualifier annotation for a {@link PlatformTransactionManager} to be injected into Batch
* auto-configuration. Can be used on a secondary {@link PlatformTransactionManager}, if
* there is another one marked as {@link Primary @Primary}.
*
* @author Lasse Wulff
* @since 4.0.0
*/
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier
public @interface BatchTransactionManager {
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2012-2025 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.JobExecution;
import org.springframework.context.ApplicationEvent;
/**
* Spring {@link ApplicationEvent} encapsulating a {@link JobExecution}.
*
* @author Dave Syer
* @since 4.0.0
*/
@SuppressWarnings("serial")
public class JobExecutionEvent extends ApplicationEvent {
private final JobExecution execution;
/**
* Create a new {@link JobExecutionEvent} instance.
* @param execution the job execution
*/
public JobExecutionEvent(JobExecution execution) {
super(execution);
this.execution = execution;
}
/**
* Return the job execution.
* @return the job execution
*/
public JobExecution getJobExecution() {
return this.execution;
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2012-2025 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.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.springframework.batch.core.JobExecution;
import org.springframework.boot.ExitCodeGenerator;
import org.springframework.context.ApplicationListener;
/**
* {@link ExitCodeGenerator} for {@link JobExecutionEvent}s.
*
* @author Dave Syer
* @since 4.0.0
*/
public class JobExecutionExitCodeGenerator implements ApplicationListener<JobExecutionEvent>, ExitCodeGenerator {
private final List<JobExecution> executions = new CopyOnWriteArrayList<>();
@Override
public void onApplicationEvent(JobExecutionEvent event) {
this.executions.add(event.getJobExecution());
}
@Override
public int getExitCode() {
for (JobExecution execution : this.executions) {
if (execution.getStatus().ordinal() > 0) {
return execution.getStatus().ordinal();
}
}
return 0;
}
}

View File

@@ -0,0 +1,251 @@
/*
* Copyright 2012-2025 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.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobExecutionException;
import org.springframework.batch.core.JobParameter;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.JobParametersInvalidException;
import org.springframework.batch.core.configuration.JobRegistry;
import org.springframework.batch.core.converter.DefaultJobParametersConverter;
import org.springframework.batch.core.converter.JobParametersConverter;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.repository.JobRestartException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.Ordered;
import org.springframework.core.log.LogMessage;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link ApplicationRunner} to {@link JobLauncher launch} Spring Batch jobs. If a single
* job is found in the context, it will be executed by default. If multiple jobs are
* found, launch a specific job by providing a jobName.
*
* @author Dave Syer
* @author Jean-Pierre Bergamin
* @author Mahmoud Ben Hassine
* @author Stephane Nicoll
* @author Akshay Dubey
* @since 4.0.0
*/
public class JobLauncherApplicationRunner
implements ApplicationRunner, InitializingBean, Ordered, ApplicationEventPublisherAware {
/**
* The default order for the command line runner.
*/
public static final int DEFAULT_ORDER = 0;
private static final Log logger = LogFactory.getLog(JobLauncherApplicationRunner.class);
private JobParametersConverter converter = new DefaultJobParametersConverter();
private final JobLauncher jobLauncher;
private final JobExplorer jobExplorer;
private final JobRepository jobRepository;
private JobRegistry jobRegistry;
private String jobName;
private Collection<Job> jobs = Collections.emptySet();
private int order = DEFAULT_ORDER;
private ApplicationEventPublisher publisher;
/**
* Create a new {@link JobLauncherApplicationRunner}.
* @param jobLauncher to launch jobs
* @param jobExplorer to check the job repository for previous executions
* @param jobRepository to check if a job instance exists with the given parameters
* when running a job
*/
public JobLauncherApplicationRunner(JobLauncher jobLauncher, JobExplorer jobExplorer, JobRepository jobRepository) {
Assert.notNull(jobLauncher, "'jobLauncher' must not be null");
Assert.notNull(jobExplorer, "'jobExplorer' must not be null");
Assert.notNull(jobRepository, "'jobRepository' must not be null");
this.jobLauncher = jobLauncher;
this.jobExplorer = jobExplorer;
this.jobRepository = jobRepository;
}
@Override
public void afterPropertiesSet() {
Assert.state(this.jobs.size() <= 1 || StringUtils.hasText(this.jobName),
"Job name must be specified in case of multiple jobs");
if (StringUtils.hasText(this.jobName)) {
Assert.state(isLocalJob(this.jobName) || isRegisteredJob(this.jobName),
() -> "No job found with name '" + this.jobName + "'");
}
}
@Deprecated(since = "3.0.10", forRemoval = true)
public void validate() {
afterPropertiesSet();
}
public void setOrder(int order) {
this.order = order;
}
@Override
public int getOrder() {
return this.order;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
@Autowired(required = false)
public void setJobRegistry(JobRegistry jobRegistry) {
this.jobRegistry = jobRegistry;
}
public void setJobName(String jobName) {
this.jobName = jobName;
}
@Autowired(required = false)
public void setJobParametersConverter(JobParametersConverter converter) {
this.converter = converter;
}
@Autowired(required = false)
public void setJobs(Collection<Job> jobs) {
this.jobs = jobs;
}
@Override
public void run(ApplicationArguments args) throws Exception {
String[] jobArguments = args.getNonOptionArgs().toArray(new String[0]);
run(jobArguments);
}
public void run(String... args) throws JobExecutionException {
logger.info("Running default command line with: " + Arrays.asList(args));
launchJobFromProperties(StringUtils.splitArrayElementsIntoProperties(args, "="));
}
protected void launchJobFromProperties(Properties properties) throws JobExecutionException {
JobParameters jobParameters = this.converter.getJobParameters(properties);
executeLocalJobs(jobParameters);
executeRegisteredJobs(jobParameters);
}
private boolean isLocalJob(String jobName) {
return this.jobs.stream().anyMatch((job) -> job.getName().equals(jobName));
}
private boolean isRegisteredJob(String jobName) {
return this.jobRegistry != null && this.jobRegistry.getJobNames().contains(jobName);
}
private void executeLocalJobs(JobParameters jobParameters) throws JobExecutionException {
for (Job job : this.jobs) {
if (StringUtils.hasText(this.jobName)) {
if (!this.jobName.equals(job.getName())) {
logger.debug(LogMessage.format("Skipped job: %s", job.getName()));
continue;
}
}
execute(job, jobParameters);
}
}
private void executeRegisteredJobs(JobParameters jobParameters) throws JobExecutionException {
if (this.jobRegistry != null && StringUtils.hasText(this.jobName)) {
if (!isLocalJob(this.jobName)) {
Job job = this.jobRegistry.getJob(this.jobName);
execute(job, jobParameters);
}
}
}
protected void execute(Job job, JobParameters jobParameters) throws JobExecutionAlreadyRunningException,
JobRestartException, JobInstanceAlreadyCompleteException, JobParametersInvalidException {
JobParameters parameters = getNextJobParameters(job, jobParameters);
JobExecution execution = this.jobLauncher.run(job, parameters);
if (this.publisher != null) {
this.publisher.publishEvent(new JobExecutionEvent(execution));
}
}
private JobParameters getNextJobParameters(Job job, JobParameters jobParameters) {
if (this.jobRepository != null && this.jobRepository.isJobInstanceExists(job.getName(), jobParameters)) {
return getNextJobParametersForExisting(job, jobParameters);
}
if (job.getJobParametersIncrementer() == null) {
return jobParameters;
}
JobParameters nextParameters = new JobParametersBuilder(jobParameters, this.jobExplorer)
.getNextJobParameters(job)
.toJobParameters();
return merge(nextParameters, jobParameters);
}
private JobParameters getNextJobParametersForExisting(Job job, JobParameters jobParameters) {
JobExecution lastExecution = this.jobRepository.getLastJobExecution(job.getName(), jobParameters);
if (isStoppedOrFailed(lastExecution) && job.isRestartable()) {
JobParameters previousIdentifyingParameters = new JobParameters(
lastExecution.getJobParameters().getIdentifyingParameters());
return merge(previousIdentifyingParameters, jobParameters);
}
return jobParameters;
}
private boolean isStoppedOrFailed(JobExecution execution) {
BatchStatus status = (execution != null) ? execution.getStatus() : null;
return (status == BatchStatus.STOPPED || status == BatchStatus.FAILED);
}
private JobParameters merge(JobParameters parameters, JobParameters additionals) {
Map<String, JobParameter<?>> merged = new LinkedHashMap<>();
merged.putAll(parameters.getParameters());
merged.putAll(additionals.getParameters());
return new JobParameters(merged);
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2012-2025 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.Collections;
import java.util.Set;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.boot.sql.init.dependency.AbstractBeansOfTypeDependsOnDatabaseInitializationDetector;
import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector;
/**
* {@link DependsOnDatabaseInitializationDetector} for Spring Batch's
* {@link JobRepository}.
*
* @author Henning Pöttker
*/
class JobRepositoryDependsOnDatabaseInitializationDetector
extends AbstractBeansOfTypeDependsOnDatabaseInitializationDetector {
@Override
protected Set<Class<?>> getDependsOnDatabaseInitializationBeanTypes() {
return Collections.singleton(JobRepository.class);
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
*/
package org.springframework.boot.batch.autoconfigure;

View File

@@ -0,0 +1,43 @@
{
"properties": [
{
"name": "spring.batch.initialize-schema",
"type": "org.springframework.boot.sql.init.DatabaseInitializationMode",
"deprecation": {
"replacement": "spring.batch.jdbc.initialize-schema",
"level": "error"
}
},
{
"name": "spring.batch.initializer.enabled",
"type": "java.lang.Boolean",
"description": "Create the required batch tables on startup if necessary. Enabled automatically\n if no custom table prefix is set or if a custom schema is configured.",
"deprecation": {
"replacement": "spring.batch.jdbc.initialize-schema",
"level": "error"
}
},
{
"name": "spring.batch.job.enabled",
"type": "java.lang.Boolean",
"description": "Execute all Spring Batch jobs in the context on startup.",
"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

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

View File

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

View File

@@ -0,0 +1,905 @@
/*
* Copyright 2012-2025 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.Arrays;
import java.util.Collection;
import java.util.Collections;
import javax.sql.DataSource;
import jakarta.persistence.EntityManagerFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.DuplicateJobException;
import org.springframework.batch.core.configuration.JobFactory;
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.explore.JobExplorer;
import org.springframework.batch.core.job.AbstractJob;
import org.springframework.batch.core.launch.JobLauncher;
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.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.DefaultApplicationArguments;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage;
import org.springframework.boot.batch.autoconfigure.BatchAutoConfiguration.SpringBootBatchConfiguration;
import org.springframework.boot.batch.autoconfigure.domain.City;
import org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration;
import org.springframework.boot.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.jpa.autoconfigure.hibernate.HibernateJpaAutoConfiguration;
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.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.ApplicationContext;
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.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Isolation;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link BatchAutoConfiguration}.
*
* @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
*/
@ExtendWith(OutputCaptureExtension.class)
class BatchAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(
AutoConfigurations.of(BatchAutoConfiguration.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(JobLauncher.class);
assertThat(context).hasSingleBean(JobExplorer.class);
assertThat(context).hasSingleBean(JobRegistry.class);
assertThat(context).hasSingleBean(JobOperator.class);
assertThat(context.getBean(BatchProperties.class).getJdbc().getInitializeSchema())
.isEqualTo(DatabaseInitializationMode.EMBEDDED);
assertThat(new JdbcTemplate(context.getBean(DataSource.class))
.queryForList("select * from BATCH_JOB_EXECUTION")).isEmpty();
});
}
@Test
void autoconfigurationBacksOffEntirelyIfSpringJdbcAbsent() {
this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class)
.withClassLoader(new FilteredClassLoader(DatabasePopulator.class))
.run((context) -> {
assertThat(context).doesNotHaveBean(JobLauncherApplicationRunner.class);
assertThat(context).doesNotHaveBean(BatchDataSourceScriptDatabaseInitializer.class);
});
}
@Test
void autoConfigurationBacksOffWhenUserEnablesBatchProcessing() {
this.contextRunner
.withUserConfiguration(EnableBatchProcessingConfiguration.class, EmbeddedDataSourceConfiguration.class)
.withClassLoader(new FilteredClassLoader(DatabasePopulator.class))
.run((context) -> assertThat(context).doesNotHaveBean(SpringBootBatchConfiguration.class));
}
@Test
void autoConfigurationBacksOffWhenUserProvidesBatchConfiguration() {
this.contextRunner.withUserConfiguration(CustomBatchConfiguration.class, EmbeddedDataSourceConfiguration.class)
.withClassLoader(new FilteredClassLoader(DatabasePopulator.class))
.run((context) -> assertThat(context).doesNotHaveBean(SpringBootBatchConfiguration.class));
}
@Test
void testDefinesAndLaunchesJob() {
this.contextRunner.withUserConfiguration(JobConfiguration.class, EmbeddedDataSourceConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(JobLauncher.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(JobLauncher.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 testDefinesAndLaunchesNamedRegisteredJob() {
this.contextRunner
.withUserConfiguration(NamedJobConfigurationWithRegisteredJob.class, EmbeddedDataSourceConfiguration.class)
.withPropertyValues("spring.batch.job.name:discreteRegisteredJob")
.run((context) -> {
assertThat(context).hasSingleBean(JobLauncher.class);
context.getBean(JobLauncherApplicationRunner.class).run();
assertThat(context.getBean(JobRepository.class)
.getLastJobExecution("discreteRegisteredJob", new JobParameters())).isNotNull();
});
}
@Test
void testRegisteredAndLocalJob() {
this.contextRunner
.withUserConfiguration(NamedJobConfigurationWithRegisteredAndLocalJob.class,
EmbeddedDataSourceConfiguration.class)
.withPropertyValues("spring.batch.job.name:discreteRegisteredJob")
.run((context) -> {
assertThat(context).hasSingleBean(JobLauncher.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(JobLauncher.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(JobLauncher.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(JobLauncher.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(JobLauncher.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(JobLauncher.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();
JobExplorer jobExplorer = context.getBean(JobExplorer.class);
assertThat(jobExplorer.findRunningJobExecutions("test")).isEmpty();
JobRepository jobRepository = context.getBean(JobRepository.class);
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
void testBatchTaskExecutor() {
this.contextRunner
.withUserConfiguration(BatchTaskExecutorConfiguration.class, EmbeddedDataSourceConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(SpringBootBatchConfiguration.class).hasBean("batchTaskExecutor");
TaskExecutor batchTaskExecutor = context.getBean("batchTaskExecutor", TaskExecutor.class);
assertThat(batchTaskExecutor).isInstanceOf(AsyncTaskExecutor.class);
assertThat(context.getBean(SpringBootBatchConfiguration.class).getTaskExecutor())
.isEqualTo(batchTaskExecutor);
assertThat(context.getBean(JobLauncher.class)).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
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(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
void customJobParametersConverterIsUsed() {
this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class)
.withBean(JobParametersConverter.class, JsonJobParametersConverter::new)
.withPropertyValues("spring.datasource.generate-unique-name=true")
.run((context) -> {
assertThat(context).hasSingleBean(JsonJobParametersConverter.class);
assertThat(context.getBean(SpringBootBatchConfiguration.class).getJobParametersConverter())
.isInstanceOf(JsonJobParametersConverter.class);
});
}
@Test
void defaultJobParametersConverterIsUsed() {
this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class).run((context) -> {
assertThat(context).doesNotHaveBean(JobParametersConverter.class);
assertThat(context.getBean(SpringBootBatchConfiguration.class).getJobParametersConverter())
.isInstanceOf(DefaultJobParametersConverter.class);
});
}
private JobLauncherApplicationRunner createInstance(String... registeredJobNames) {
JobLauncherApplicationRunner runner = new JobLauncherApplicationRunner(mock(JobLauncher.class),
mock(JobExplorer.class), mock(JobRepository.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 NamedJobConfigurationWithRegisteredJob {
@Bean
static BeanPostProcessor registryProcessor(ApplicationContext applicationContext) {
return new NamedJobJobRegistryBeanPostProcessor(applicationContext);
}
}
static class NamedJobJobRegistryBeanPostProcessor implements BeanPostProcessor {
private final ApplicationContext applicationContext;
NamedJobJobRegistryBeanPostProcessor(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof JobRegistry jobRegistry) {
try {
jobRegistry.register(getJobFactory());
}
catch (DuplicateJobException ex) {
// Ignore
}
}
return bean;
}
private JobFactory getJobFactory() {
JobRepository jobRepository = this.applicationContext.getBean(JobRepository.class);
return new JobFactory() {
@Override
public Job createJob() {
AbstractJob job = new AbstractJob("discreteRegisteredJob") {
@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(jobRepository);
return job;
}
@Override
public String getJobName() {
return "discreteRegisteredJob";
}
};
}
}
@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)
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

@@ -0,0 +1,103 @@
/*
* Copyright 2012-2025 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 javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage;
import org.springframework.boot.batch.autoconfigure.BatchAutoConfiguration.SpringBootBatchConfiguration;
import org.springframework.boot.batch.autoconfigure.domain.City;
import org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.jdbc.autoconfigure.EmbeddedDataSourceConfiguration;
import org.springframework.boot.sql.init.DatabaseInitializationMode;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
import org.springframework.boot.transaction.autoconfigure.TransactionAutoConfiguration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.Isolation;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link BatchAutoConfiguration} when JPA is not on the classpath.
*
* @author Stephane Nicoll
*/
@ClassPathExclusions("hibernate-jpa-*.jar")
class BatchAutoConfigurationWithoutJpaTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(BatchAutoConfiguration.class, TransactionAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class));
@Test
void jdbcWithDefaultSettings() {
this.contextRunner.withUserConfiguration(DefaultConfiguration.class, EmbeddedDataSourceConfiguration.class)
.withPropertyValues("spring.datasource.generate-unique-name=true")
.run((context) -> {
assertThat(context).hasSingleBean(JobLauncher.class);
assertThat(context).hasSingleBean(JobExplorer.class);
assertThat(context).hasSingleBean(JobRepository.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();
assertThat(context.getBean(JobExplorer.class).findRunningJobExecutions("test")).isEmpty();
assertThat(context.getBean(JobRepository.class).getLastJobExecution("test", new JobParameters()))
.isNull();
});
}
@Test
@WithPackageResources("custom-schema.sql")
void jdbcWithCustomPrefix() {
this.contextRunner.withUserConfiguration(DefaultConfiguration.class, EmbeddedDataSourceConfiguration.class)
.withPropertyValues("spring.datasource.generate-unique-name=true",
"spring.batch.jdbc.schema:classpath:custom-schema.sql", "spring.batch.jdbc.tablePrefix:PREFIX_")
.run((context) -> {
assertThat(new JdbcTemplate(context.getBean(DataSource.class))
.queryForList("select * from PREFIX_JOB_EXECUTION")).isEmpty();
assertThat(context.getBean(JobExplorer.class).findRunningJobExecutions("test")).isEmpty();
assertThat(context.getBean(JobRepository.class).getLastJobExecution("test", new JobParameters()))
.isNull();
});
}
@Test
void jdbcWithCustomIsolationLevel() {
this.contextRunner.withUserConfiguration(DefaultConfiguration.class, EmbeddedDataSourceConfiguration.class)
.withPropertyValues("spring.datasource.generate-unique-name=true",
"spring.batch.jdbc.isolation-level-for-create=read_committed")
.run((context) -> assertThat(
context.getBean(SpringBootBatchConfiguration.class).getIsolationLevelForCreate())
.isEqualTo(Isolation.READ_COMMITTED));
}
@TestAutoConfigurationPackage(City.class)
static class DefaultConfiguration {
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2012-2025 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.io.IOException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.List;
import java.util.stream.Stream;
import javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.EnumSource.Mode;
import org.springframework.boot.jdbc.DatabaseDriver;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link BatchDataSourceScriptDatabaseInitializer}.
*
* @author Stephane Nicoll
*/
class BatchDataSourceScriptDatabaseInitializerTests {
@Test
void getSettingsWithPlatformDoesNotTouchDataSource() {
DataSource dataSource = mock(DataSource.class);
BatchProperties properties = new BatchProperties();
properties.getJdbc().setPlatform("test");
DatabaseInitializationSettings settings = BatchDataSourceScriptDatabaseInitializer.getSettings(dataSource,
properties.getJdbc());
assertThat(settings.getSchemaLocations())
.containsOnly("classpath:org/springframework/batch/core/schema-test.sql");
then(dataSource).shouldHaveNoInteractions();
}
@ParameterizedTest
@EnumSource(value = DatabaseDriver.class, mode = Mode.EXCLUDE, names = { "AWS_WRAPPER", "CLICKHOUSE", "FIREBIRD",
"INFORMIX", "JTDS", "PHOENIX", "REDSHIFT", "TERADATA", "TESTCONTAINERS", "UNKNOWN" })
void batchSchemaCanBeLocated(DatabaseDriver driver) throws SQLException {
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
BatchProperties properties = new BatchProperties();
DataSource dataSource = mock(DataSource.class);
Connection connection = mock(Connection.class);
given(dataSource.getConnection()).willReturn(connection);
DatabaseMetaData metadata = mock(DatabaseMetaData.class);
given(connection.getMetaData()).willReturn(metadata);
String productName = (String) ReflectionTestUtils.getField(driver, "productName");
given(metadata.getDatabaseProductName()).willReturn(productName);
DatabaseInitializationSettings settings = BatchDataSourceScriptDatabaseInitializer.getSettings(dataSource,
properties.getJdbc());
List<String> schemaLocations = settings.getSchemaLocations();
assertThat(schemaLocations).isNotEmpty()
.allSatisfy((location) -> assertThat(resourceLoader.getResource(location).exists()).isTrue());
}
@Test
void batchHasExpectedBuiltInSchemas() throws IOException {
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
List<String> schemaNames = Stream
.of(resolver.getResources("classpath:org/springframework/batch/core/schema-*.sql"))
.map(Resource::getFilename)
.filter((resourceName) -> !resourceName.contains("-drop-"))
.toList();
assertThat(schemaNames).containsExactlyInAnyOrder("schema-derby.sql", "schema-sqlserver.sql",
"schema-mariadb.sql", "schema-mysql.sql", "schema-sqlite.sql", "schema-postgresql.sql",
"schema-hana.sql", "schema-oracle.sql", "schema-db2.sql", "schema-hsqldb.sql", "schema-sybase.sql",
"schema-h2.sql");
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2012-2025 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.junit.jupiter.api.Test;
import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link BatchProperties}.
*
* @author Andy Wilkinson
*/
class BatchPropertiesTests {
@Test
void validateTransactionStateDefaultMatchesSpringBatchDefault() {
assertThat(new BatchProperties().getJdbc().isValidateTransactionState())
.isEqualTo(new TestBatchConfiguration().getValidateTransactionState());
}
static class TestBatchConfiguration extends DefaultBatchConfiguration {
@Override
public boolean getValidateTransactionState() {
return super.getValidateTransactionState();
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2012-2025 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.junit.jupiter.api.Test;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.JobExecution;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JobExecutionExitCodeGenerator}.
*
* @author Dave Syer
*/
class JobExecutionExitCodeGeneratorTests {
private final JobExecutionExitCodeGenerator generator = new JobExecutionExitCodeGenerator();
@Test
void testExitCodeForRunning() {
this.generator.onApplicationEvent(new JobExecutionEvent(new JobExecution(0L)));
assertThat(this.generator.getExitCode()).isOne();
}
@Test
void testExitCodeForCompleted() {
JobExecution execution = new JobExecution(0L);
execution.setStatus(BatchStatus.COMPLETED);
this.generator.onApplicationEvent(new JobExecutionEvent(execution));
assertThat(this.generator.getExitCode()).isZero();
}
@Test
void testExitCodeForFailed() {
JobExecution execution = new JobExecution(0L);
execution.setStatus(BatchStatus.FAILED);
this.generator.onApplicationEvent(new JobExecutionEvent(execution));
assertThat(this.generator.getExitCode()).isEqualTo(5);
}
}

View File

@@ -0,0 +1,247 @@
/*
* Copyright 2012-2025 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.Arrays;
import java.util.List;
import javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecutionException;
import org.springframework.batch.core.JobInstance;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.job.builder.SimpleJobBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.repository.JobRestartException;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration;
import org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.transaction.autoconfigure.TransactionAutoConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.fail;
/**
* Tests for {@link JobLauncherApplicationRunner}.
*
* @author Dave Syer
* @author Jean-Pierre Bergamin
* @author Mahmoud Ben Hassine
* @author Stephane Nicoll
*/
class JobLauncherApplicationRunnerTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, TransactionAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class))
.withUserConfiguration(BatchConfiguration.class);
@Test
void basicExecution() {
this.contextRunner.run((context) -> {
JobLauncherApplicationRunnerContext jobLauncherContext = new JobLauncherApplicationRunnerContext(context);
jobLauncherContext.executeJob(new JobParameters());
assertThat(jobLauncherContext.jobInstances()).hasSize(1);
jobLauncherContext.executeJob(new JobParametersBuilder().addLong("id", 1L).toJobParameters());
assertThat(jobLauncherContext.jobInstances()).hasSize(2);
});
}
@Test
void incrementExistingExecution() {
this.contextRunner.run((context) -> {
JobLauncherApplicationRunnerContext jobLauncherContext = new JobLauncherApplicationRunnerContext(context);
Job job = jobLauncherContext.configureJob().incrementer(new RunIdIncrementer()).build();
jobLauncherContext.runner.execute(job, new JobParameters());
jobLauncherContext.runner.execute(job, new JobParameters());
assertThat(jobLauncherContext.jobInstances()).hasSize(2);
});
}
@Test
void retryFailedExecution() {
this.contextRunner.run((context) -> {
PlatformTransactionManager transactionManager = context.getBean(PlatformTransactionManager.class);
JobLauncherApplicationRunnerContext jobLauncherContext = new JobLauncherApplicationRunnerContext(context);
Job job = jobLauncherContext.jobBuilder()
.start(jobLauncherContext.stepBuilder().tasklet(throwingTasklet(), transactionManager).build())
.incrementer(new RunIdIncrementer())
.build();
jobLauncherContext.runner.execute(job, new JobParameters());
jobLauncherContext.runner.execute(job, new JobParametersBuilder().addLong("run.id", 1L).toJobParameters());
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())
.incrementer(new RunIdIncrementer())
.build();
jobLauncherContext.runner.execute(job, new JobParameters());
jobLauncherContext.runner.execute(job, new JobParameters());
// A failed job that is not restartable does not re-use the job params of
// the last execution, but creates a new job instance when running it again.
assertThat(jobLauncherContext.jobInstances()).hasSize(2);
assertThatExceptionOfType(JobRestartException.class).isThrownBy(() -> {
// try to re-run a failed execution
jobLauncherContext.runner.execute(job,
new JobParametersBuilder().addLong("run.id", 1L).toJobParameters());
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())
.incrementer(new RunIdIncrementer())
.build();
JobParameters jobParameters = new JobParametersBuilder().addLong("id", 1L, false)
.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);
});
}
private Tasklet throwingTasklet() {
return (contribution, chunkContext) -> {
throw new RuntimeException("Planned");
};
}
static class JobLauncherApplicationRunnerContext {
private final JobLauncherApplicationRunner runner;
private final JobExplorer jobExplorer;
private final JobBuilder jobBuilder;
private final Job job;
private final StepBuilder stepBuilder;
private final Step step;
JobLauncherApplicationRunnerContext(ApplicationContext context) {
JobLauncher jobLauncher = context.getBean(JobLauncher.class);
JobRepository jobRepository = context.getBean(JobRepository.class);
PlatformTransactionManager transactionManager = context.getBean(PlatformTransactionManager.class);
this.stepBuilder = new StepBuilder("step", jobRepository);
this.step = this.stepBuilder.tasklet((contribution, chunkContext) -> null, transactionManager).build();
this.jobBuilder = new JobBuilder("job", jobRepository);
this.job = this.jobBuilder.start(this.step).build();
this.jobExplorer = context.getBean(JobExplorer.class);
this.runner = new JobLauncherApplicationRunner(jobLauncher, this.jobExplorer, jobRepository);
}
List<JobInstance> jobInstances() {
return this.jobExplorer.getJobInstances("job", 0, 100);
}
void executeJob(JobParameters jobParameters) throws JobExecutionException {
this.runner.execute(this.job, jobParameters);
}
JobBuilder jobBuilder() {
return this.jobBuilder;
}
StepBuilder stepBuilder() {
return this.stepBuilder;
}
SimpleJobBuilder configureJob() {
return this.jobBuilder.start(this.step);
}
}
@EnableBatchProcessing
@Configuration(proxyBeanMethods = false)
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

@@ -0,0 +1,78 @@
/*
* Copyright 2012-2025 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.domain;
import java.io.Serializable;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
@Entity
public class City implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String state;
@Column(nullable = false)
private String country;
@Column(nullable = false)
private String map;
protected City() {
}
public City(String name, String state, String country, String map) {
this.name = name;
this.state = state;
this.country = country;
this.map = map;
}
public String getName() {
return this.name;
}
public String getState() {
return this.state;
}
public String getCountry() {
return this.country;
}
public String getMap() {
return this.map;
}
@Override
public String toString() {
return getName() + "," + getState() + "," + getCountry();
}
}

View File

@@ -0,0 +1,85 @@
CREATE TABLE PREFIX_JOB_INSTANCE (
JOB_INSTANCE_ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
VERSION BIGINT,
JOB_NAME VARCHAR(100) NOT NULL,
JOB_KEY VARCHAR(32) NOT NULL,
constraint JOB_INST_UN unique (JOB_NAME, JOB_KEY)
) ;
CREATE TABLE PREFIX_JOB_EXECUTION (
JOB_EXECUTION_ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
VERSION BIGINT,
JOB_INSTANCE_ID BIGINT NOT NULL,
CREATE_TIME TIMESTAMP NOT NULL,
START_TIME TIMESTAMP DEFAULT NULL,
END_TIME TIMESTAMP DEFAULT NULL,
STATUS VARCHAR(10),
EXIT_CODE VARCHAR(2500),
EXIT_MESSAGE VARCHAR(2500),
LAST_UPDATED TIMESTAMP,
JOB_CONFIGURATION_LOCATION VARCHAR(2500) NULL,
constraint JOB_INST_EXEC_FK foreign key (JOB_INSTANCE_ID)
references PREFIX_JOB_INSTANCE(JOB_INSTANCE_ID)
) ;
CREATE TABLE PREFIX_JOB_EXECUTION_PARAMS (
JOB_EXECUTION_ID BIGINT NOT NULL,
TYPE_CD VARCHAR(6) NOT NULL,
KEY_NAME VARCHAR(100) NOT NULL,
STRING_VAL VARCHAR(250),
DATE_VAL TIMESTAMP DEFAULT NULL,
LONG_VAL BIGINT,
DOUBLE_VAL DOUBLE PRECISION,
IDENTIFYING CHAR(1) NOT NULL,
constraint JOB_EXEC_PARAMS_FK foreign key (JOB_EXECUTION_ID)
references PREFIX_JOB_EXECUTION(JOB_EXECUTION_ID)
) ;
CREATE TABLE PREFIX_STEP_EXECUTION (
STEP_EXECUTION_ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
VERSION BIGINT NOT NULL,
STEP_NAME VARCHAR(100) NOT NULL,
JOB_EXECUTION_ID BIGINT NOT NULL,
START_TIME TIMESTAMP NOT NULL,
END_TIME TIMESTAMP DEFAULT NULL,
STATUS VARCHAR(10),
COMMIT_COUNT BIGINT,
READ_COUNT BIGINT,
FILTER_COUNT BIGINT,
WRITE_COUNT BIGINT,
READ_SKIP_COUNT BIGINT,
WRITE_SKIP_COUNT BIGINT,
PROCESS_SKIP_COUNT BIGINT,
ROLLBACK_COUNT BIGINT,
EXIT_CODE VARCHAR(2500),
EXIT_MESSAGE VARCHAR(2500),
LAST_UPDATED TIMESTAMP,
constraint JOB_EXEC_STEP_FK foreign key (JOB_EXECUTION_ID)
references PREFIX_JOB_EXECUTION(JOB_EXECUTION_ID)
) ;
CREATE TABLE PREFIX_STEP_EXECUTION_CONTEXT (
STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY,
SHORT_CONTEXT VARCHAR(2500) NOT NULL,
SERIALIZED_CONTEXT LONGVARCHAR,
constraint STEP_EXEC_CTX_FK foreign key (STEP_EXECUTION_ID)
references PREFIX_STEP_EXECUTION(STEP_EXECUTION_ID)
) ;
CREATE TABLE PREFIX_JOB_EXECUTION_CONTEXT (
JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY,
SHORT_CONTEXT VARCHAR(2500) NOT NULL,
SERIALIZED_CONTEXT LONGVARCHAR,
constraint JOB_EXEC_CTX_FK foreign key (JOB_EXECUTION_ID)
references PREFIX_JOB_EXECUTION(JOB_EXECUTION_ID)
) ;
CREATE TABLE PREFIX_STEP_EXECUTION_SEQ (
ID BIGINT GENERATED BY DEFAULT AS IDENTITY
);
CREATE TABLE PREFIX_JOB_EXECUTION_SEQ (
ID BIGINT GENERATED BY DEFAULT AS IDENTITY
);
CREATE TABLE PREFIX_JOB_SEQ (
ID BIGINT GENERATED BY DEFAULT AS IDENTITY
);