diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java
index 5583d46396..b59d074c9d 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java
@@ -45,15 +45,14 @@ import org.springframework.jdbc.datasource.init.DatabasePopulator;
import org.springframework.util.StringUtils;
/**
- * {@link EnableAutoConfiguration Auto-configuration} for Spring Batch. By default a
- * Runner will be created and all jobs in the context will be executed on startup.
+ * {@link EnableAutoConfiguration Auto-configuration} for Spring Batch. If a single job is
+ * found in the context, it will be executed on startup.
*
* Disable this behavior with {@literal spring.batch.job.enabled=false}).
*
- * Alternatively, discrete Job names to execute on startup can be supplied by the User
- * with a comma-delimited list: {@literal spring.batch.job.names=job1,job2}. In this case
- * the Runner will first find jobs registered as Beans, then those in the existing
- * JobRegistry.
+ * 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
@@ -74,9 +73,9 @@ public class BatchAutoConfiguration {
public JobLauncherApplicationRunner jobLauncherApplicationRunner(JobLauncher jobLauncher, JobExplorer jobExplorer,
JobRepository jobRepository, BatchProperties properties) {
JobLauncherApplicationRunner runner = new JobLauncherApplicationRunner(jobLauncher, jobExplorer, jobRepository);
- String jobNames = properties.getJob().getNames();
+ String jobNames = properties.getJob().getName();
if (StringUtils.hasText(jobNames)) {
- runner.setJobNames(jobNames);
+ runner.setJobName(jobNames);
}
return runner;
}
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchProperties.java
index efe9a140b2..2422b3b42f 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchProperties.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchProperties.java
@@ -46,17 +46,17 @@ public class BatchProperties {
public static class Job {
/**
- * Comma-separated list of job names to execute on startup (for instance,
- * 'job1,job2'). By default, all Jobs found in the context are executed.
+ * Job name to execute on startup. Must be specified if multiple Jobs are found in
+ * the context.
*/
- private String names = "";
+ private String name = "";
- public String getNames() {
- return this.names;
+ public String getName() {
+ return this.name;
}
- public void setNames(String names) {
- this.names = names;
+ public void setName(String name) {
+ this.name = name;
}
}
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobLauncherApplicationRunner.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobLauncherApplicationRunner.java
index d443e6e24c..10873a1191 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobLauncherApplicationRunner.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobLauncherApplicationRunner.java
@@ -24,6 +24,7 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
+import jakarta.annotation.PostConstruct;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -53,7 +54,6 @@ 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.PatternMatchUtils;
import org.springframework.util.StringUtils;
/**
@@ -86,7 +86,7 @@ public class JobLauncherApplicationRunner implements ApplicationRunner, Ordered,
private JobRegistry jobRegistry;
- private String jobNames;
+ private String jobName;
private Collection jobs = Collections.emptySet();
@@ -110,6 +110,13 @@ public class JobLauncherApplicationRunner implements ApplicationRunner, Ordered,
this.jobRepository = jobRepository;
}
+ @PostConstruct
+ public void validate() {
+ if (this.jobs.size() > 1 && !StringUtils.hasText(this.jobName)) {
+ throw new IllegalArgumentException("Job name must be specified in case of multiple jobs");
+ }
+ }
+
public void setOrder(int order) {
this.order = order;
}
@@ -129,8 +136,8 @@ public class JobLauncherApplicationRunner implements ApplicationRunner, Ordered,
this.jobRegistry = jobRegistry;
}
- public void setJobNames(String jobNames) {
- this.jobNames = jobNames;
+ public void setJobName(String jobName) {
+ this.jobName = jobName;
}
@Autowired(required = false)
@@ -162,9 +169,8 @@ public class JobLauncherApplicationRunner implements ApplicationRunner, Ordered,
private void executeLocalJobs(JobParameters jobParameters) throws JobExecutionException {
for (Job job : this.jobs) {
- if (StringUtils.hasText(this.jobNames)) {
- String[] jobsToRun = this.jobNames.split(",");
- if (!PatternMatchUtils.simpleMatch(jobsToRun, job.getName())) {
+ if (StringUtils.hasText(this.jobName)) {
+ if (!this.jobName.equals(job.getName())) {
logger.debug(LogMessage.format("Skipped job: %s", job.getName()));
continue;
}
@@ -174,19 +180,15 @@ public class JobLauncherApplicationRunner implements ApplicationRunner, Ordered,
}
private void executeRegisteredJobs(JobParameters jobParameters) throws JobExecutionException {
- if (this.jobRegistry != null && StringUtils.hasText(this.jobNames)) {
- String[] jobsToRun = this.jobNames.split(",");
- for (String jobName : jobsToRun) {
- try {
- Job job = this.jobRegistry.getJob(jobName);
- if (this.jobs.contains(job)) {
- continue;
- }
+ if (this.jobRegistry != null && StringUtils.hasText(this.jobName)) {
+ try {
+ Job job = this.jobRegistry.getJob(this.jobName);
+ if (!this.jobs.contains(job)) {
execute(job, jobParameters);
}
- catch (NoSuchJobException ex) {
- logger.debug(LogMessage.format("No job found in registry for job name: %s", jobName));
- }
+ }
+ catch (NoSuchJobException ex) {
+ logger.debug(LogMessage.format("No job found in registry for job name: %s", this.jobName));
}
}
}
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java
index 1b81c7fcf1..0fa400d2ab 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java
@@ -24,6 +24,7 @@ import javax.sql.DataSource;
import jakarta.persistence.EntityManagerFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mockito;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.Job;
@@ -31,6 +32,8 @@ 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.BatchConfigurer;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
@@ -39,7 +42,9 @@ 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.repository.JobRepository;
+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;
@@ -60,6 +65,7 @@ import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
+import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@@ -138,11 +144,11 @@ class BatchAutoConfigurationTests {
}
@Test
- void testDefinesAndLaunchesNamedJob() {
+ void testDefinesAndLaunchesNamedRegisteredJob() {
this.contextRunner
.withUserConfiguration(NamedJobConfigurationWithRegisteredJob.class,
EmbeddedDataSourceConfiguration.class)
- .withPropertyValues("spring.batch.job.names:discreteRegisteredJob").run((context) -> {
+ .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",
@@ -150,11 +156,46 @@ class BatchAutoConfigurationTests {
});
}
+ @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.names:discreteLocalJob").run((context) -> {
+ .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",
@@ -395,18 +436,15 @@ class BatchAutoConfigurationTests {
@Configuration(proxyBeanMethods = false)
@EnableBatchProcessing
- static class NamedJobConfigurationWithRegisteredJob {
-
- @Autowired
- private JobRegistry jobRegistry;
+ static class NamedJobConfigurationWithRegisteredAndLocalJob {
@Autowired
private JobRepository jobRepository;
@Bean
- JobRegistryBeanPostProcessor registryProcessor() {
+ static JobRegistryBeanPostProcessor registryProcessor(JobRegistry jobRegistry) {
JobRegistryBeanPostProcessor processor = new JobRegistryBeanPostProcessor();
- processor.setJobRegistry(this.jobRegistry);
+ processor.setJobRegistry(jobRegistry);
return processor;
}
@@ -414,6 +452,8 @@ class BatchAutoConfigurationTests {
Job discreteJob() {
AbstractJob job = new AbstractJob("discreteRegisteredJob") {
+ private static int count = 0;
+
@Override
public Collection getStepNames() {
return Collections.emptySet();
@@ -426,7 +466,13 @@ class BatchAutoConfigurationTests {
@Override
protected void doExecute(JobExecution execution) {
- execution.setStatus(BatchStatus.COMPLETED);
+ if (count == 0) {
+ execution.setStatus(BatchStatus.COMPLETED);
+ }
+ else {
+ execution.setStatus(BatchStatus.FAILED);
+ }
+ count++;
}
};
job.setJobRepository(this.jobRepository);
@@ -435,6 +481,75 @@ class BatchAutoConfigurationTests {
}
+ @Configuration(proxyBeanMethods = false)
+ @EnableBatchProcessing
+ 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) {
+ }
+ }
+ 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 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)
@EnableBatchProcessing
static class NamedJobConfigurationWithLocalJob {
@@ -467,6 +582,43 @@ class BatchAutoConfigurationTests {
}
+ @Configuration(proxyBeanMethods = false)
+ @EnableBatchProcessing
+ static class MultipleJobConfiguration {
+
+ @Autowired
+ private JobRepository jobRepository;
+
+ @Bean
+ Job discreteJob() {
+ AbstractJob job = new AbstractJob("discreteLocalJob") {
+
+ @Override
+ public Collection getStepNames() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public Step getStep(String stepName) {
+ return null;
+ }
+
+ @Override
+ protected void doExecute(JobExecution execution) {
+ execution.setStatus(BatchStatus.COMPLETED);
+ }
+ };
+ job.setJobRepository(this.jobRepository);
+ return job;
+ }
+
+ @Bean
+ Job job2() {
+ return Mockito.mock(Job.class);
+ }
+
+ }
+
@Configuration(proxyBeanMethods = false)
@EnableBatchProcessing
static class JobConfiguration {
diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc
index 3b492be462..a9b1b4fe43 100644
--- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc
+++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc
@@ -22,8 +22,10 @@ For more info about Spring Batch, see the {spring-batch}[Spring Batch project pa
=== Running Spring Batch Jobs on Startup
Spring Batch auto-configuration is enabled by adding `@EnableBatchProcessing` to one of your `@Configuration` classes.
-By default, it executes *all* `Jobs` in the application context on startup (see {spring-boot-autoconfigure-module-code}/batch/JobLauncherApplicationRunner.java[`JobLauncherApplicationRunner`] for details).
-You can narrow down to a specific job or jobs by specifying `spring.batch.job.names` (which takes a comma-separated list of job name patterns).
+If a single `Job` is found in the application context, it is executed on startup (see {spring-boot-autoconfigure-module-code}/batch/JobLauncherApplicationRunner.java[`JobLauncherApplicationRunner`] for details).
+If multiple `Job`s are found, the job that should be executed must be specified using configprop:spring.batch.job.name[].
+
+To disable running a `Job` found in the application content, set the configprop:spring.batch.job.enabled[] to `false.`
See {spring-boot-autoconfigure-module-code}/batch/BatchAutoConfiguration.java[BatchAutoConfiguration] and {spring-batch-api}/core/configuration/annotation/EnableBatchProcessing.html[@EnableBatchProcessing] for more details.