diff --git a/spring-cloud-task-batch/pom.xml b/spring-cloud-task-batch/pom.xml
index 9d542a4a..9822728b 100644
--- a/spring-cloud-task-batch/pom.xml
+++ b/spring-cloud-task-batch/pom.xml
@@ -70,5 +70,14 @@
spring-boot-configuration-processor
true
+
+ org.assertj
+ assertj-core
+ test
+
+
+ org.springframework.boot
+ spring-boot-test
+
diff --git a/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskBatchAutoConfiguration.java b/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskBatchAutoConfiguration.java
index e76439f2..bae7d5c3 100644
--- a/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskBatchAutoConfiguration.java
+++ b/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskBatchAutoConfiguration.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016 the original author or authors.
+ * Copyright 2016-2018 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.
@@ -56,14 +56,11 @@ public class TaskBatchAutoConfiguration {
@Autowired
private ApplicationContext context;
- @Autowired
- private TaskProperties taskProperties;
-
@Bean
public TaskBatchExecutionListenerFactoryBean taskBatchExecutionListener(TaskExplorer taskExplorer) {
TaskConfigurer taskConfigurer = null;
- if(!context.getBeansOfType(TaskConfigurer.class).isEmpty()) {
- taskConfigurer = context.getBean(TaskConfigurer.class);
+ if(!this.context.getBeansOfType(TaskConfigurer.class).isEmpty()) {
+ taskConfigurer = this.context.getBean(TaskConfigurer.class);
}
if(taskConfigurer != null && taskConfigurer.getTaskDataSource() != null) {
return new TaskBatchExecutionListenerFactoryBean(
@@ -74,6 +71,5 @@ public class TaskBatchAutoConfiguration {
return new TaskBatchExecutionListenerFactoryBean(null, taskExplorer);
}
}
-
}
}
diff --git a/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskBatchProperties.java b/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskBatchProperties.java
new file mode 100644
index 00000000..ebbed8e6
--- /dev/null
+++ b/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskBatchProperties.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018 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
+ *
+ * http://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.cloud.task.batch.configuration;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Establish properties to be used for how Tasks work with
+ * Spring Batch.
+ *
+ * @author Glenn Renfro
+ */
+@ConfigurationProperties(prefix = "spring.cloud.task.batch")
+public class TaskBatchProperties {
+
+ /**
+ * Comma-separated list of job names to execute on startup (for instance,
+ * `job1,job2`). By default, all Jobs found in the context are executed.
+ */
+ private String jobNames = "";
+
+ public String getJobNames() {
+ return this.jobNames;
+ }
+
+ public void setJobNames(String jobNames) {
+ this.jobNames = jobNames;
+ }
+}
diff --git a/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherAutoConfiguration.java b/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherAutoConfiguration.java
new file mode 100644
index 00000000..8f811783
--- /dev/null
+++ b/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherAutoConfiguration.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2018 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
+ *
+ * http://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.cloud.task.batch.configuration;
+
+import java.util.List;
+
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.configuration.JobRegistry;
+import org.springframework.batch.core.explore.JobExplorer;
+import org.springframework.batch.core.launch.JobLauncher;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Provides auto configuration for the
+ * {@link org.springframework.cloud.task.batch.handler.TaskJobLauncherCommandLineRunner}.
+ *
+ * @author Glenn Renfro
+ */
+@Configuration
+@ConditionalOnProperty(name = "spring.cloud.task.batch.commandLineRunnerEnabled", havingValue = "true", matchIfMissing = false)
+@EnableConfigurationProperties(TaskBatchProperties.class)
+public class TaskJobLauncherAutoConfiguration {
+
+ @Autowired
+ private TaskBatchProperties properties;
+
+ @Bean
+ public TaskJobLauncherCommandLineRunnerFactoryBean jobLauncherCommandLineRunner(JobLauncher jobLauncher,
+ JobExplorer jobExplorer, List jobs, JobRegistry jobRegistry) {
+ TaskJobLauncherCommandLineRunnerFactoryBean taskJobLauncherCommandLineRunner = new TaskJobLauncherCommandLineRunnerFactoryBean(
+ jobLauncher, jobExplorer, jobs, this.properties.getJobNames(), jobRegistry);
+ return taskJobLauncherCommandLineRunner;
+ }
+}
diff --git a/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherCommandLineRunnerFactoryBean.java b/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherCommandLineRunnerFactoryBean.java
new file mode 100644
index 00000000..3e209148
--- /dev/null
+++ b/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherCommandLineRunnerFactoryBean.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2018 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
+ *
+ * http://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.cloud.task.batch.configuration;
+
+import java.util.List;
+
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.configuration.JobRegistry;
+import org.springframework.batch.core.explore.JobExplorer;
+import org.springframework.batch.core.launch.JobLauncher;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.cloud.task.batch.handler.TaskJobLauncherCommandLineRunner;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Factory bean for creating an instance of {@link TaskJobLauncherCommandLineRunner}.
+ *
+ * @author Glenn Renfro
+ */
+public class TaskJobLauncherCommandLineRunnerFactoryBean implements FactoryBean {
+
+ private JobLauncher jobLauncher;
+
+ private JobExplorer jobExplorer;
+
+ private List jobs;
+
+ private String jobNames;
+
+ private JobRegistry jobRegistry;
+
+ public TaskJobLauncherCommandLineRunnerFactoryBean(JobLauncher jobLauncher,
+ JobExplorer jobExplorer, List jobs, String jobNames,
+ JobRegistry jobRegistry) {
+ this.jobLauncher = jobLauncher;
+ this.jobExplorer = jobExplorer;
+ Assert.notEmpty(jobs, "jobs must not be null nor empty");
+ this.jobs = jobs;
+ this.jobNames = jobNames;
+ this.jobRegistry = jobRegistry;
+ }
+
+ @Override
+ public TaskJobLauncherCommandLineRunner getObject() throws Exception {
+ TaskJobLauncherCommandLineRunner taskJobLauncherCommandLineRunner =
+ new TaskJobLauncherCommandLineRunner(this.jobLauncher, this.jobExplorer);
+ taskJobLauncherCommandLineRunner.setJobs(this.jobs);
+ if(StringUtils.hasText(this.jobNames)) {
+ taskJobLauncherCommandLineRunner.setJobNames(this.jobNames);
+ }
+ taskJobLauncherCommandLineRunner.setJobRegistry(this.jobRegistry);
+ return taskJobLauncherCommandLineRunner;
+ }
+
+ @Override
+ public Class> getObjectType() {
+ return TaskJobLauncherCommandLineRunner.class;
+ }
+}
diff --git a/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunner.java b/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunner.java
new file mode 100644
index 00000000..fb3c54a7
--- /dev/null
+++ b/spring-cloud-task-batch/src/main/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunner.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2018 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
+ *
+ * http://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.cloud.task.batch.handler;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Properties;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.batch.core.ExitStatus;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobExecutionException;
+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.launch.JobParametersNotFoundException;
+import org.springframework.batch.core.launch.NoSuchJobException;
+import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
+import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
+import org.springframework.batch.core.repository.JobRestartException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.autoconfigure.batch.JobExecutionEvent;
+import org.springframework.cloud.task.listener.TaskException;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.ApplicationEventPublisherAware;
+import org.springframework.core.Ordered;
+import org.springframework.util.PatternMatchUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * {@link CommandLineRunner} to {@link JobLauncher launch} Spring Batch jobs. Runs all
+ * jobs in the surrounding context by default and throw an exception upon the
+ * first job that returns an {@link ExitStatus} of FAILED.
+ * Can also be used to launch a specific job by providing a jobName. The
+ * TaskJobLaunchercommandLineRunner takes the place of the
+ * {@link org.springframework.boot.autoconfigure.batch.JobLauncherCommandLineRunner}
+ * when it is in use.
+ *
+ * @author Glenn Renfro
+ * @since 2.0.0
+ */
+public class TaskJobLauncherCommandLineRunner implements CommandLineRunner, Ordered, ApplicationEventPublisherAware{
+ /**
+ * The default order for the command line runner.
+ */
+ public static final int DEFAULT_ORDER = 0;
+
+ private static final Log logger = LogFactory
+ .getLog(TaskJobLauncherCommandLineRunner.class);
+
+ private JobParametersConverter converter = new DefaultJobParametersConverter();
+
+ private JobLauncher jobLauncher;
+
+ private JobRegistry jobRegistry;
+
+ private JobExplorer jobExplorer;
+
+ private String jobNames;
+
+ private Collection jobs = Collections.emptySet();
+
+ private int order = DEFAULT_ORDER;
+
+ private ApplicationEventPublisher publisher;
+
+ public TaskJobLauncherCommandLineRunner(JobLauncher jobLauncher,
+ JobExplorer jobExplorer) {
+ this.jobLauncher = jobLauncher;
+ this.jobExplorer = jobExplorer;
+ }
+
+ public void setOrder(int order) {
+ this.order = order;
+ }
+
+ @Override
+ public int getOrder() {
+ return this.order;
+ }
+
+ @Override
+ public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
+ this.publisher = publisher;
+ }
+
+ public void setJobRegistry(JobRegistry jobRegistry) {
+ this.jobRegistry = jobRegistry;
+ }
+
+ public void setJobNames(String jobNames) {
+ this.jobNames = jobNames;
+ }
+
+ public void setJobParametersConverter(JobParametersConverter converter) {
+ this.converter = converter;
+ }
+
+ public void setJobs(Collection jobs) {
+ this.jobs = jobs;
+ }
+
+ @Override
+ 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 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;
+ }
+ execute(job, jobParameters);
+ }
+ catch (NoSuchJobException ex) {
+ logger.debug("No job found in registry for job name: " + jobName);
+ }
+ }
+ }
+ }
+
+ protected void execute(Job job, JobParameters jobParameters)
+ throws JobExecutionAlreadyRunningException, JobRestartException,
+ JobInstanceAlreadyCompleteException, JobParametersInvalidException,
+ JobParametersNotFoundException {
+ JobParameters nextParameters = new JobParametersBuilder(jobParameters,
+ this.jobExplorer).getNextJobParameters(job).toJobParameters();
+ JobExecution execution = this.jobLauncher.run(job, nextParameters);
+ if (this.publisher != null) {
+ this.publisher.publishEvent(new JobExecutionEvent(execution));
+ }
+ if(execution.getExitStatus().getExitCode().equals(ExitStatus.FAILED.getExitCode())) {
+ String message = String.format("Job %s failed during " +
+ "execution for jobId %s with jobExecutionId of %s",
+ execution.getJobInstance().getJobName(),
+ execution.getJobId(), execution.getId());
+ logger.error(message);
+ throw new TaskException(message);
+ }
+ }
+
+ 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())) {
+ logger.debug("Skipped job: " + job.getName());
+ continue;
+ }
+ }
+ execute(job, jobParameters);
+ }
+ }
+}
diff --git a/spring-cloud-task-batch/src/main/resources/META-INF/spring.factories b/spring-cloud-task-batch/src/main/resources/META-INF/spring.factories
index 262efdef..3db9c9fe 100644
--- a/spring-cloud-task-batch/src/main/resources/META-INF/spring.factories
+++ b/spring-cloud-task-batch/src/main/resources/META-INF/spring.factories
@@ -1 +1 @@
-org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.springframework.cloud.task.batch.configuration.TaskBatchAutoConfiguration
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.springframework.cloud.task.batch.configuration.TaskBatchAutoConfiguration,org.springframework.cloud.task.batch.configuration.TaskJobLauncherAutoConfiguration
diff --git a/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherAutoConfigurationTests.java b/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherAutoConfigurationTests.java
new file mode 100644
index 00000000..83e3a9e4
--- /dev/null
+++ b/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/configuration/TaskJobLauncherAutoConfigurationTests.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018 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
+ *
+ * http://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.cloud.task.batch.configuration;
+
+import org.junit.Test;
+
+import org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration;
+import org.springframework.boot.autoconfigure.batch.JobLauncherCommandLineRunner;
+import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.cloud.task.batch.handler.TaskJobLauncherCommandLineRunner;
+import org.springframework.cloud.task.batch.listener.TaskBatchExecutionListenerTests;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Glenn Renfro
+ */
+public class TaskJobLauncherAutoConfigurationTests {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().
+ withUserConfiguration(TaskBatchExecutionListenerTests.JobConfiguration.class,
+ PropertyPlaceholderAutoConfiguration.class,
+ EmbeddedDataSourceConfiguration.class,
+ BatchAutoConfiguration.class,
+ TaskJobLauncherAutoConfiguration.class);
+
+ @Test
+ public void testAutoBuiltDataSourceWithTaskJobLauncherCLR() {
+ this.contextRunner.
+ withPropertyValues("spring.cloud.task.batch.commandLineRunnerEnabled=true").
+ run(context -> {
+ assertThat(context).hasSingleBean(TaskJobLauncherCommandLineRunner.class);
+ assertThat(context).doesNotHaveBean(JobLauncherCommandLineRunner.class);
+ });
+ }
+
+ @Test
+ public void testAutoBuiltDataSourceWithTaskJobLauncherCLRDisabled() {
+ this.contextRunner.run(context -> {
+ assertThat(context).hasSingleBean(JobLauncherCommandLineRunner.class);
+ assertThat(context).doesNotHaveBean(TaskJobLauncherCommandLineRunner.class);
+ });
+ }
+}
diff --git a/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunnerCoreTests.java b/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunnerCoreTests.java
new file mode 100644
index 00000000..bbb04417
--- /dev/null
+++ b/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunnerCoreTests.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2018 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
+ *
+ * http://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.cloud.task.batch.handler;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.springframework.batch.core.Job;
+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.BatchConfigurer;
+import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
+import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
+import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
+import org.springframework.batch.core.explore.JobExplorer;
+import org.springframework.batch.core.explore.support.MapJobExplorerFactoryBean;
+import org.springframework.batch.core.launch.JobLauncher;
+import org.springframework.batch.core.launch.support.RunIdIncrementer;
+import org.springframework.batch.core.launch.support.SimpleJobLauncher;
+import org.springframework.batch.core.repository.JobRepository;
+import org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean;
+import org.springframework.batch.core.step.tasklet.Tasklet;
+import org.springframework.batch.support.transaction.ResourcelessTransactionManager;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.task.listener.TaskException;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.task.SyncTaskExecutor;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.transaction.PlatformTransactionManager;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Glenn Renfro
+ */
+@RunWith(SpringRunner.class)
+@ContextConfiguration(classes = {TaskJobLauncherCommandLineRunnerCoreTests.BatchConfiguration.class})
+public class TaskJobLauncherCommandLineRunnerCoreTests {
+
+ @Autowired
+ private JobRepository jobRepository;
+
+ @Autowired
+ private JobLauncher jobLauncher;
+
+ @Autowired
+ private JobExplorer jobExplorer;
+
+ @Autowired
+ PlatformTransactionManager transactionManager;
+
+ private TaskJobLauncherCommandLineRunner runner;
+
+ private JobBuilderFactory jobs;
+
+ private StepBuilderFactory steps;
+
+ private Job job;
+
+ private Step step;
+
+ @Before
+ public void init() {
+ this.jobs = new JobBuilderFactory(this.jobRepository);
+ this.steps = new StepBuilderFactory(this.jobRepository, this.transactionManager);
+ Tasklet tasklet = (contribution, chunkContext) -> null;
+ this.step = this.steps.get("step").tasklet(tasklet).build();
+ this.job = this.jobs.get("job").start(this.step).build();
+ this.runner = new TaskJobLauncherCommandLineRunner(this.jobLauncher, this.jobExplorer);
+ }
+
+
+ @DirtiesContext
+ @Test
+ public void basicExecution() throws Exception {
+ this.runner.execute(this.job, new JobParameters());
+ assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(1);
+ this.runner.execute(this.job,
+ new JobParametersBuilder().addLong("id", 1L).toJobParameters());
+ assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(2);
+ }
+
+ @DirtiesContext
+ @Test
+ public void incrementExistingExecution() throws Exception {
+ this.job = this.jobs.get("job").start(this.step)
+ .incrementer(new RunIdIncrementer()).build();
+ this.runner.execute(this.job, new JobParameters());
+ this.runner.execute(this.job, new JobParameters());
+ assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(2);
+ }
+
+ @DirtiesContext
+ @Test
+ public void retryFailedExecution() throws Exception {
+ this.job = this.jobs.get("job")
+ .start(this.steps.get("step").tasklet(throwingTasklet()).build())
+ .incrementer(new RunIdIncrementer()).build();
+ runFailedJob(new JobParameters());
+ runFailedJob(new JobParameters());
+ assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(1);
+ }
+
+ @DirtiesContext
+ @Test
+ public void retryFailedExecutionOnNonRestartableJob() throws Exception {
+ this.job = this.jobs.get("job").preventRestart()
+ .start(this.steps.get("step").tasklet(throwingTasklet()).build())
+ .incrementer(new RunIdIncrementer()).build();
+ runFailedJob(new JobParameters());
+ runFailedJob(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(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(2);
+ }
+
+ @DirtiesContext
+ @Test
+ public void retryFailedExecutionWithNonIdentifyingParameters() throws Exception {
+ this.job = this.jobs.get("job")
+ .start(this.steps.get("step").tasklet(throwingTasklet()).build())
+ .incrementer(new RunIdIncrementer()).build();
+ JobParameters jobParameters = new JobParametersBuilder().addLong("id", 1L, false)
+ .addLong("foo", 2L, false).toJobParameters();
+ runFailedJob(new JobParameters());
+ runFailedJob(new JobParameters());
+ assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(1);
+ }
+
+ private Tasklet throwingTasklet() {
+ return (contribution, chunkContext) -> {
+ throw new RuntimeException("Planned");
+ };
+ }
+
+ private void runFailedJob(JobParameters jobParameters) throws Exception {
+ boolean isExceptionThrown = false;
+ try {
+ this.runner.execute(this.job, new JobParameters());
+ }
+ catch (TaskException taskException) {
+ isExceptionThrown = true;
+ }
+ assertThat(isExceptionThrown).isTrue();
+ }
+
+ @Configuration
+ @EnableBatchProcessing
+ protected static class BatchConfiguration implements BatchConfigurer {
+
+ private ResourcelessTransactionManager transactionManager =
+ new ResourcelessTransactionManager();
+
+ private JobRepository jobRepository;
+
+ private MapJobRepositoryFactoryBean jobRepositoryFactory =
+ new MapJobRepositoryFactoryBean(
+ this.transactionManager);
+
+ public BatchConfiguration() throws Exception {
+ this.jobRepository = this.jobRepositoryFactory.getObject();
+ }
+
+ public void clear() {
+ this.jobRepositoryFactory.clear();
+ }
+
+ @Override
+ public JobRepository getJobRepository() {
+ return this.jobRepository;
+ }
+
+ @Override
+ public PlatformTransactionManager getTransactionManager() {
+ return this.transactionManager;
+ }
+
+ @Override
+ public JobLauncher getJobLauncher() {
+ SimpleJobLauncher launcher = new SimpleJobLauncher();
+ launcher.setJobRepository(this.jobRepository);
+ launcher.setTaskExecutor(new SyncTaskExecutor());
+ return launcher;
+ }
+
+ @Override
+ public JobExplorer getJobExplorer() throws Exception {
+ return new MapJobExplorerFactoryBean(this.jobRepositoryFactory).getObject();
+ }
+
+ }
+}
diff --git a/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunnerTests.java b/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunnerTests.java
new file mode 100644
index 00000000..90353184
--- /dev/null
+++ b/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/handler/TaskJobLauncherCommandLineRunnerTests.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2018 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
+ *
+ * http://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.cloud.task.batch.handler;
+
+import java.util.Set;
+
+import org.assertj.core.api.Assertions;
+import org.junit.After;
+import org.junit.Test;
+
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.StepContribution;
+import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
+import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
+import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
+import org.springframework.batch.core.scope.context.ChunkContext;
+import org.springframework.batch.core.step.tasklet.Tasklet;
+import org.springframework.batch.repeat.RepeatStatus;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration;
+import org.springframework.boot.autoconfigure.batch.JobLauncherCommandLineRunner;
+import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.cloud.task.batch.configuration.TaskBatchAutoConfiguration;
+import org.springframework.cloud.task.batch.configuration.TaskJobLauncherAutoConfiguration;
+import org.springframework.cloud.task.configuration.EnableTask;
+import org.springframework.cloud.task.repository.TaskExecution;
+import org.springframework.cloud.task.repository.TaskExplorer;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+/**
+ * @author Glenn Renfro
+ */
+public class TaskJobLauncherCommandLineRunnerTests {
+
+ private ConfigurableApplicationContext applicationContext;
+
+ @After
+ public void tearDown() {
+ if (this.applicationContext != null) {
+ this.applicationContext.close();
+ }
+ }
+
+ @Test
+ public void testTaskJobLauncherCLRSuccessFail() {
+ String[] enabledArgs = new String[] { "--spring.cloud.task.batch.commandLineRunnerEnabled=true" };
+ boolean isExceptionThrown = false;
+ try {
+ this.applicationContext = SpringApplication
+ .run(new Class[] { TaskJobLauncherCommandLineRunnerTests.JobWithFailureConfiguration.class,
+ PropertyPlaceholderAutoConfiguration.class,
+ EmbeddedDataSourceConfiguration.class,
+ BatchAutoConfiguration.class,
+ TaskBatchAutoConfiguration.class,
+ TaskJobLauncherAutoConfiguration.class }, enabledArgs);
+ }
+ catch (IllegalStateException exception) {
+ isExceptionThrown = true;
+ }
+ assertThat(isExceptionThrown).isTrue();
+ }
+
+ @Test
+ public void testTaskJobLauncherPickOneJob() {
+ String[] enabledArgs = new String[] {
+ "--spring.cloud.task.batch.commandLineRunnerEnabled=true",
+ "--spring.cloud.task.batch.jobNames=jobSucceed" };
+ boolean isExceptionThrown = false;
+ try {
+ this.applicationContext = SpringApplication
+ .run(new Class[] { TaskJobLauncherCommandLineRunnerTests.JobWithFailureConfiguration.class,
+ PropertyPlaceholderAutoConfiguration.class,
+ EmbeddedDataSourceConfiguration.class,
+ BatchAutoConfiguration.class,
+ TaskBatchAutoConfiguration.class,
+ TaskJobLauncherAutoConfiguration.class }, enabledArgs);
+ }
+ catch (IllegalStateException exception) {
+ isExceptionThrown = true;
+ }
+ assertThat(isExceptionThrown).isFalse();
+ validateContext();
+ }
+
+ @Test
+ public void testCommandLineRunnerSetToFalse() {
+ String[] enabledArgs = new String[] { };
+ this.applicationContext = SpringApplication
+ .run(new Class[] { TaskJobLauncherCommandLineRunnerTests.JobConfiguration.class,
+ PropertyPlaceholderAutoConfiguration.class,
+ EmbeddedDataSourceConfiguration.class,
+ BatchAutoConfiguration.class,
+ TaskBatchAutoConfiguration.class,
+ TaskJobLauncherAutoConfiguration.class }, enabledArgs);
+ validateContext();
+ assertThat(applicationContext.getBean(JobLauncherCommandLineRunner.class)).isNotNull();
+ boolean exceptionThrown = false;
+ try {
+ applicationContext.getBean(TaskJobLauncherCommandLineRunner.class);
+ }
+ catch (NoSuchBeanDefinitionException exception) {
+ exceptionThrown = true;
+ }
+ assertThat(exceptionThrown).isTrue();
+ validateContext();
+ }
+
+ private void validateContext() {
+ TaskExplorer taskExplorer = this.applicationContext.getBean(TaskExplorer.class);
+
+ Page page = taskExplorer.findTaskExecutionsByName("application", PageRequest.of(0, 1));
+
+ Set jobExecutionIds = taskExplorer
+ .getJobExecutionIdsByTaskExecutionId(page.iterator().next().getExecutionId());
+
+ assertThat(jobExecutionIds.size()).isEqualTo(1);
+ assertThat(taskExplorer.getTaskExecution(jobExecutionIds.iterator().next()).getExecutionId()).isEqualTo(1);
+
+ }
+
+ @Configuration
+ @EnableBatchProcessing
+ @EnableTask
+ public static class JobConfiguration {
+
+ @Autowired
+ private JobBuilderFactory jobBuilderFactory;
+
+ @Autowired
+ private StepBuilderFactory stepBuilderFactory;
+
+ @Bean
+ public Job job() {
+ return jobBuilderFactory.get("job")
+ .start(stepBuilderFactory.get("step1").tasklet(new Tasklet() {
+ @Override
+ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext)
+ throws Exception {
+ System.out.println("Executed");
+ return RepeatStatus.FINISHED;
+ }
+ }).build())
+ .build();
+ }
+ }
+
+ @Configuration
+ @EnableBatchProcessing
+ @EnableTask
+ public static class JobWithFailureConfiguration {
+
+ @Autowired
+ private JobBuilderFactory jobBuilderFactory;
+
+ @Autowired
+ private StepBuilderFactory stepBuilderFactory;
+
+ @Bean
+ public Job jobFail() {
+ return jobBuilderFactory.get("jobA")
+ .start(stepBuilderFactory.get("step1").tasklet(new Tasklet() {
+ @Override
+ public RepeatStatus execute(StepContribution contribution,
+ ChunkContext chunkContext)
+ throws Exception {
+ System.out.println("Executed");
+ throw new IllegalStateException("WHOOPS");
+ }
+ }).build())
+ .build();
+ }
+
+ @Bean
+ public Job jobFun() {
+ return jobBuilderFactory.get("jobSucceed")
+ .start(stepBuilderFactory.get("step1Succeed").tasklet(new Tasklet() {
+ @Override
+ public RepeatStatus execute(StepContribution contribution,
+ ChunkContext chunkContext)
+ throws Exception {
+ System.out.println("Executed");
+ return RepeatStatus.FINISHED;
+ }
+ }).build())
+ .build();
+ }
+ }
+}
diff --git a/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/listener/TaskBatchExecutionListenerTests.java b/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/listener/TaskBatchExecutionListenerTests.java
index 52dcf98d..6c613cd4 100644
--- a/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/listener/TaskBatchExecutionListenerTests.java
+++ b/spring-cloud-task-batch/src/test/java/org/springframework/cloud/task/batch/listener/TaskBatchExecutionListenerTests.java
@@ -65,7 +65,7 @@ import static org.junit.Assert.assertEquals;
*/
public class TaskBatchExecutionListenerTests {
- public static final String[] ARGS = new String[] {"--spring.cloud.task.closecontext_enable=false"};
+ public static final String[] ARGS = new String[] {};
private ConfigurableApplicationContext applicationContext;
diff --git a/spring-cloud-task-docs/src/main/asciidoc/batch.adoc b/spring-cloud-task-docs/src/main/asciidoc/batch.adoc
index 5be59741..9b58f9be 100644
--- a/spring-cloud-task-docs/src/main/asciidoc/batch.adoc
+++ b/spring-cloud-task-docs/src/main/asciidoc/batch.adoc
@@ -243,8 +243,15 @@ is covered in detail in the section
As discussed before Spring Cloud Task applications support the ability to record the exit code of a task execution.
However in cases where a user is running a Spring Batch Job within a task, regardless of how the Batch Job
-Execution completes the result of the task will always be zero when using default Batch/Boot behavior. Keep in mind
-that a task is a boot application and the exit code returned from the task is the same as a boot application. So
-to have your task return the exit code based on the result of the batch job execution, you will need to write your
-own CommandLineRunner.
+Execution completes the result of the task will always be zero when using default Batch/Boot behavior.
+Keep in mind that a task is a boot application and the exit code returned from
+the task is the same as a boot application.
+To override this behavior and allow the task to return an exit code other than zero
+(either 1 `the default` or based on the
+https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-spring-application.html#boot-features-application-exit[ExitCodeGenerator]
+specified) upon a batch job returning an
+https://docs.spring.io/spring-batch/4.0.x/reference/html/step.html#conditionalFlow[ExitStatus]
+of "FAILED", set `spring.cloud.task.batch.commandLineRunnerEnabled` to true.
+To have your task return the exit code based on the result of the batch job
+execution, you will need to write your own `CommandLineRunner`.