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

View File

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

View File

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

View File

@ -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<String> 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<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)
@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<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)
@EnableBatchProcessing
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
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.