Remove support for multiple batch jobs

Closes gh-25373
This commit is contained in:
Madhura Bhave
2022-06-10 14:34:02 -07:00
parent fabe0637cd
commit 55d6a87fef
5 changed files with 200 additions and 45 deletions

View File

@@ -45,15 +45,14 @@ import org.springframework.jdbc.datasource.init.DatabasePopulator;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
* {@link EnableAutoConfiguration Auto-configuration} for Spring Batch. By default a * {@link EnableAutoConfiguration Auto-configuration} for Spring Batch. If a single job is
* Runner will be created and all jobs in the context will be executed on startup. * found in the context, it will be executed on startup.
* <p> * <p>
* Disable this behavior with {@literal spring.batch.job.enabled=false}). * Disable this behavior with {@literal spring.batch.job.enabled=false}).
* <p> * <p>
* Alternatively, discrete Job names to execute on startup can be supplied by the User * If multiple jobs are found, a job name to execute on startup can be supplied by the
* with a comma-delimited list: {@literal spring.batch.job.names=job1,job2}. In this case * User with : {@literal spring.batch.job.name=job1}. In this case the Runner will first
* the Runner will first find jobs registered as Beans, then those in the existing * find jobs registered as Beans, then those in the existing JobRegistry.
* JobRegistry.
* *
* @author Dave Syer * @author Dave Syer
* @author Eddú Meléndez * @author Eddú Meléndez
@@ -74,9 +73,9 @@ public class BatchAutoConfiguration {
public JobLauncherApplicationRunner jobLauncherApplicationRunner(JobLauncher jobLauncher, JobExplorer jobExplorer, public JobLauncherApplicationRunner jobLauncherApplicationRunner(JobLauncher jobLauncher, JobExplorer jobExplorer,
JobRepository jobRepository, BatchProperties properties) { JobRepository jobRepository, BatchProperties properties) {
JobLauncherApplicationRunner runner = new JobLauncherApplicationRunner(jobLauncher, jobExplorer, jobRepository); JobLauncherApplicationRunner runner = new JobLauncherApplicationRunner(jobLauncher, jobExplorer, jobRepository);
String jobNames = properties.getJob().getNames(); String jobNames = properties.getJob().getName();
if (StringUtils.hasText(jobNames)) { if (StringUtils.hasText(jobNames)) {
runner.setJobNames(jobNames); runner.setJobName(jobNames);
} }
return runner; return runner;
} }

View File

@@ -46,17 +46,17 @@ public class BatchProperties {
public static class Job { public static class Job {
/** /**
* Comma-separated list of job names to execute on startup (for instance, * Job name to execute on startup. Must be specified if multiple Jobs are found in
* 'job1,job2'). By default, all Jobs found in the context are executed. * the context.
*/ */
private String names = ""; private String name = "";
public String getNames() { public String getName() {
return this.names; return this.name;
} }
public void setNames(String names) { public void setName(String name) {
this.names = names; this.name = name;
} }
} }

View File

@@ -24,6 +24,7 @@ import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
import jakarta.annotation.PostConstruct;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
@@ -53,7 +54,6 @@ import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.log.LogMessage; import org.springframework.core.log.LogMessage;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.PatternMatchUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
@@ -86,7 +86,7 @@ public class JobLauncherApplicationRunner implements ApplicationRunner, Ordered,
private JobRegistry jobRegistry; private JobRegistry jobRegistry;
private String jobNames; private String jobName;
private Collection<Job> jobs = Collections.emptySet(); private Collection<Job> jobs = Collections.emptySet();
@@ -110,6 +110,13 @@ public class JobLauncherApplicationRunner implements ApplicationRunner, Ordered,
this.jobRepository = jobRepository; 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) { public void setOrder(int order) {
this.order = order; this.order = order;
} }
@@ -129,8 +136,8 @@ public class JobLauncherApplicationRunner implements ApplicationRunner, Ordered,
this.jobRegistry = jobRegistry; this.jobRegistry = jobRegistry;
} }
public void setJobNames(String jobNames) { public void setJobName(String jobName) {
this.jobNames = jobNames; this.jobName = jobName;
} }
@Autowired(required = false) @Autowired(required = false)
@@ -162,9 +169,8 @@ public class JobLauncherApplicationRunner implements ApplicationRunner, Ordered,
private void executeLocalJobs(JobParameters jobParameters) throws JobExecutionException { private void executeLocalJobs(JobParameters jobParameters) throws JobExecutionException {
for (Job job : this.jobs) { for (Job job : this.jobs) {
if (StringUtils.hasText(this.jobNames)) { if (StringUtils.hasText(this.jobName)) {
String[] jobsToRun = this.jobNames.split(","); if (!this.jobName.equals(job.getName())) {
if (!PatternMatchUtils.simpleMatch(jobsToRun, job.getName())) {
logger.debug(LogMessage.format("Skipped job: %s", job.getName())); logger.debug(LogMessage.format("Skipped job: %s", job.getName()));
continue; continue;
} }
@@ -174,19 +180,15 @@ public class JobLauncherApplicationRunner implements ApplicationRunner, Ordered,
} }
private void executeRegisteredJobs(JobParameters jobParameters) throws JobExecutionException { private void executeRegisteredJobs(JobParameters jobParameters) throws JobExecutionException {
if (this.jobRegistry != null && StringUtils.hasText(this.jobNames)) { if (this.jobRegistry != null && StringUtils.hasText(this.jobName)) {
String[] jobsToRun = this.jobNames.split(","); try {
for (String jobName : jobsToRun) { Job job = this.jobRegistry.getJob(this.jobName);
try { if (!this.jobs.contains(job)) {
Job job = this.jobRegistry.getJob(jobName);
if (this.jobs.contains(job)) {
continue;
}
execute(job, jobParameters); 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));
} }
} }
} }

View File

@@ -24,6 +24,7 @@ import javax.sql.DataSource;
import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.EntityManagerFactory;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.Job; 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.JobParameters;
import org.springframework.batch.core.JobParametersBuilder; import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.Step; 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.JobRegistry;
import org.springframework.batch.core.configuration.annotation.BatchConfigurer; import org.springframework.batch.core.configuration.annotation.BatchConfigurer;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; 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.job.AbstractJob;
import org.springframework.batch.core.launch.JobLauncher; import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.repository.JobRepository;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.CommandLineRunner; import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.DefaultApplicationArguments; 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.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
@@ -138,11 +144,11 @@ class BatchAutoConfigurationTests {
} }
@Test @Test
void testDefinesAndLaunchesNamedJob() { void testDefinesAndLaunchesNamedRegisteredJob() {
this.contextRunner this.contextRunner
.withUserConfiguration(NamedJobConfigurationWithRegisteredJob.class, .withUserConfiguration(NamedJobConfigurationWithRegisteredJob.class,
EmbeddedDataSourceConfiguration.class) EmbeddedDataSourceConfiguration.class)
.withPropertyValues("spring.batch.job.names:discreteRegisteredJob").run((context) -> { .withPropertyValues("spring.batch.job.name:discreteRegisteredJob").run((context) -> {
assertThat(context).hasSingleBean(JobLauncher.class); assertThat(context).hasSingleBean(JobLauncher.class);
context.getBean(JobLauncherApplicationRunner.class).run(); context.getBean(JobLauncherApplicationRunner.class).run();
assertThat(context.getBean(JobRepository.class).getLastJobExecution("discreteRegisteredJob", 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 @Test
void testDefinesAndLaunchesLocalJob() { void testDefinesAndLaunchesLocalJob() {
this.contextRunner this.contextRunner
.withUserConfiguration(NamedJobConfigurationWithLocalJob.class, EmbeddedDataSourceConfiguration.class) .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); assertThat(context).hasSingleBean(JobLauncher.class);
context.getBean(JobLauncherApplicationRunner.class).run(); context.getBean(JobLauncherApplicationRunner.class).run();
assertThat(context.getBean(JobRepository.class).getLastJobExecution("discreteLocalJob", assertThat(context.getBean(JobRepository.class).getLastJobExecution("discreteLocalJob",
@@ -395,18 +436,15 @@ class BatchAutoConfigurationTests {
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@EnableBatchProcessing @EnableBatchProcessing
static class NamedJobConfigurationWithRegisteredJob { static class NamedJobConfigurationWithRegisteredAndLocalJob {
@Autowired
private JobRegistry jobRegistry;
@Autowired @Autowired
private JobRepository jobRepository; private JobRepository jobRepository;
@Bean @Bean
JobRegistryBeanPostProcessor registryProcessor() { static JobRegistryBeanPostProcessor registryProcessor(JobRegistry jobRegistry) {
JobRegistryBeanPostProcessor processor = new JobRegistryBeanPostProcessor(); JobRegistryBeanPostProcessor processor = new JobRegistryBeanPostProcessor();
processor.setJobRegistry(this.jobRegistry); processor.setJobRegistry(jobRegistry);
return processor; return processor;
} }
@@ -414,6 +452,8 @@ class BatchAutoConfigurationTests {
Job discreteJob() { Job discreteJob() {
AbstractJob job = new AbstractJob("discreteRegisteredJob") { AbstractJob job = new AbstractJob("discreteRegisteredJob") {
private static int count = 0;
@Override @Override
public Collection<String> getStepNames() { public Collection<String> getStepNames() {
return Collections.emptySet(); return Collections.emptySet();
@@ -426,7 +466,13 @@ class BatchAutoConfigurationTests {
@Override @Override
protected void doExecute(JobExecution execution) { 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); 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<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) @Configuration(proxyBeanMethods = false)
@EnableBatchProcessing @EnableBatchProcessing
static class NamedJobConfigurationWithLocalJob { 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<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 Mockito.mock(Job.class);
}
}
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@EnableBatchProcessing @EnableBatchProcessing
static class JobConfiguration { static class JobConfiguration {

View File

@@ -22,8 +22,10 @@ For more info about Spring Batch, see the {spring-batch}[Spring Batch project pa
=== Running Spring Batch Jobs on Startup === Running Spring Batch Jobs on Startup
Spring Batch auto-configuration is enabled by adding `@EnableBatchProcessing` to one of your `@Configuration` classes. 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). 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).
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 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. See {spring-boot-autoconfigure-module-code}/batch/BatchAutoConfiguration.java[BatchAutoConfiguration] and {spring-batch-api}/core/configuration/annotation/EnableBatchProcessing.html[@EnableBatchProcessing] for more details.