From 55d6a87fefcd1f258a0ca5bc104b82b3bebf8aa1 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Fri, 10 Jun 2022 14:34:02 -0700 Subject: [PATCH] Remove support for multiple batch jobs Closes gh-25373 --- .../batch/BatchAutoConfiguration.java | 15 +- .../autoconfigure/batch/BatchProperties.java | 14 +- .../batch/JobLauncherApplicationRunner.java | 38 ++-- .../batch/BatchAutoConfigurationTests.java | 172 +++++++++++++++++- .../src/docs/asciidoc/howto/batch.adoc | 6 +- 5 files changed, 200 insertions(+), 45 deletions(-) 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.