Add listener to persist relationship between job and task

This introduces a listener that stores the association between a Spring
Batch job and the task it was executed within.

Resolves spring-cloud/spring-cloud-task#46

Merge Changes based on code review.
This commit is contained in:
Michael Minella
2016-03-04 14:52:21 -06:00
committed by Glenn Renfro
parent bb76cef391
commit 7c8fc5f50e
39 changed files with 1579 additions and 17 deletions

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2016 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.batch.core.Job;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.task.batch.listener.TaskBatchExecutionListener;
import org.springframework.cloud.task.configuration.EnableTask;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Provides auto configuration for the {@link TaskBatchExecutionListener}.
*
* @author Michael Minella
*/
@Configuration
@ConditionalOnClass({Job.class, EnableTask.class})
public class TaskBatchAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public TaskBatchExecutionListenerBeanPostProcessor batchTaskExecutionListenerBeanPostProcessor() {
return new TaskBatchExecutionListenerBeanPostProcessor();
}
@Bean
@ConditionalOnMissingBean
public TaskBatchExecutionListenerFactoryBean taskBatchExecutionListener(ConfigurableApplicationContext context) {
return new TaskBatchExecutionListenerFactoryBean(context);
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2016 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.batch.core.job.AbstractJob;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.cloud.task.batch.listener.TaskBatchExecutionListener;
import org.springframework.context.ApplicationContext;
/**
* Injects a configured {@link TaskBatchExecutionListener} into any batch jobs (beans
* assignable to {@link AbstractJob}) that are executed within the scope of a task. The
* context this is used within is expected to have only one bean of type
* {@link TaskBatchExecutionListener}.
*
* @author Michael Minella
*/
public class TaskBatchExecutionListenerBeanPostProcessor implements BeanPostProcessor {
@Autowired
private ApplicationContext applicationContext;
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
int length = this.applicationContext
.getBeanNamesForType(TaskBatchExecutionListener.class).length;
if(bean instanceof AbstractJob) {
if(length != 1) {
throw new IllegalStateException("The application context is required to " +
"have exactly 1 instance of the TaskBatchExecutionListener but has " +
length);
}
((AbstractJob) bean).registerJobExecutionListener(
this.applicationContext.getBean(TaskBatchExecutionListener.class));
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2016 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.lang.reflect.Field;
import javax.sql.DataSource;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.cloud.task.batch.listener.TaskBatchExecutionListener;
import org.springframework.cloud.task.batch.listener.support.JdbcTaskBatchDao;
import org.springframework.cloud.task.batch.listener.support.MapTaskBatchDao;
import org.springframework.cloud.task.repository.TaskExplorer;
import org.springframework.cloud.task.repository.dao.MapTaskExecutionDao;
import org.springframework.cloud.task.repository.support.SimpleTaskExplorer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* {@link FactoryBean} for a {@link TaskBatchExecutionListener}. Provides a jdbc based
* listener if there is a {@link DataSource} available. Otherwise, builds a listener that
* uses the map based implementation.
*
* @author Michael Minella
*/
public class TaskBatchExecutionListenerFactoryBean implements FactoryBean<TaskBatchExecutionListener> {
private ConfigurableApplicationContext context;
private TaskBatchExecutionListener listener;
private String dataSourceName;
/**
* @param context the current application context
*/
public TaskBatchExecutionListenerFactoryBean(ConfigurableApplicationContext context) {
Assert.notNull(context, "A ConfigurableApplicationContext is required");
this.context = context;
}
@Override
public TaskBatchExecutionListener getObject() throws Exception {
if(listener != null){
return listener;
}
if(this.context.getBeanNamesForType(DataSource.class).length == 0) {
this.listener = new TaskBatchExecutionListener(getMapTaskBatchDao());
}
else {
DataSource dataSource;
if(StringUtils.hasText(this.dataSourceName)) {
dataSource = (DataSource) this.context.getBean(this.dataSourceName);
}
else {
if(this.context.getBeanNamesForType(DataSource.class).length == 1) {
dataSource = this.context.getBean(DataSource.class);
}
else {
throw new IllegalStateException("Unable to determine what DataSource to use");
}
}
this.listener = new TaskBatchExecutionListener(new JdbcTaskBatchDao(dataSource));
}
return listener;
}
@Override
public Class<?> getObjectType() {
return TaskBatchExecutionListener.class;
}
@Override
public boolean isSingleton() {
return true;
}
public void setDataSourceName(String dataSourceName) {
this.dataSourceName = dataSourceName;
}
private MapTaskBatchDao getMapTaskBatchDao() throws Exception {
Field taskExecutionDaoField = ReflectionUtils.findField(SimpleTaskExplorer.class, "taskExecutionDao");
taskExecutionDaoField.setAccessible(true);
MapTaskExecutionDao taskExecutionDao;
TaskExplorer taskExplorer = this.context.getBean(TaskExplorer.class);
if(AopUtils.isJdkDynamicProxy(taskExplorer)) {
SimpleTaskExplorer dereferencedTaskRepository = (SimpleTaskExplorer) ((Advised) taskExplorer).getTargetSource().getTarget();
taskExecutionDao =
(MapTaskExecutionDao) ReflectionUtils.getField(taskExecutionDaoField, dereferencedTaskRepository);
}
else {
taskExecutionDao =
(MapTaskExecutionDao) ReflectionUtils.getField(taskExecutionDaoField, taskExplorer);
}
return new MapTaskBatchDao(taskExecutionDao.getBatchJobAssociations());
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2016 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.listener;
import org.springframework.batch.core.JobExecution;
import org.springframework.cloud.task.repository.TaskExecution;
/**
* Maintains the association between a {@link TaskExecution} and a {@link JobExecution}
* executed within it.
*
* @author Michael Minella
*/
public interface TaskBatchDao {
/**
* Saves the relationship between a task execution and a job execution.
*
* @param taskExecution task execution
* @param jobExecution job execution
*/
void saveRelationship(TaskExecution taskExecution, JobExecution jobExecution);
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2016 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.listener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.listener.JobExecutionListenerSupport;
import org.springframework.cloud.task.listener.annotation.BeforeTask;
import org.springframework.cloud.task.repository.TaskExecution;
import org.springframework.util.Assert;
/**
* Responsible for storing the relationship between a Spring Batch job and the Spring
* Cloud task it was executed within.
*
* @author Michael Minella
*/
public class TaskBatchExecutionListener extends JobExecutionListenerSupport {
private TaskExecution taskExecution;
private TaskBatchDao taskBatchDao;
private final static Log logger = LogFactory.getLog(TaskBatchExecutionListener.class);
/**
* @param taskBatchDao dao used to persist the relationship. Must not be null
*/
public TaskBatchExecutionListener(TaskBatchDao taskBatchDao) {
Assert.notNull(taskBatchDao, "A TaskBatchDao is required");
this.taskBatchDao = taskBatchDao;
}
@BeforeTask
public void onTaskStartup(TaskExecution taskExecution) {
this.taskExecution = taskExecution;
}
@Override
public void beforeJob(JobExecution jobExecution) {
if(this.taskExecution == null) {
logger.warn("This job was executed outside the scope of a task but still used the task listener.");
}
else {
logger.info(String.format("The job execution id %s was run within the task execution %s",
jobExecution.getId(),
this.taskExecution.getExecutionId()));
taskBatchDao.saveRelationship(taskExecution, jobExecution);
}
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2016 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.listener.support;
import javax.sql.DataSource;
import org.springframework.batch.core.JobExecution;
import org.springframework.cloud.task.batch.listener.TaskBatchDao;
import org.springframework.cloud.task.repository.TaskExecution;
import org.springframework.cloud.task.repository.dao.JdbcTaskExecutionDao;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* JDBC based implementation of the {@link TaskBatchDao}. Intended to be used in
* conjunction with the JDBC based
* {@link org.springframework.cloud.task.repository.TaskRepository}
*
* @author Michael Minella
*/
public class JdbcTaskBatchDao implements TaskBatchDao {
private String tablePrefix = JdbcTaskExecutionDao.DEFAULT_TABLE_PREFIX;
private static final String INSERT_STATEMENT = "INSERT INTO %PREFIX%TASK_BATCH VALUES(?, ?)";
private JdbcOperations jdbcTemplate;
/**
* @param dataSource {@link DataSource} where the task batch table resides.
*/
public JdbcTaskBatchDao(DataSource dataSource) {
Assert.notNull(dataSource, "A dataSource is required");
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public void saveRelationship(TaskExecution taskExecution, JobExecution jobExecution) {
Assert.notNull(taskExecution, "A taskExecution is required");
Assert.notNull(jobExecution, "A jobExecution is required");
jdbcTemplate.update(getQuery(INSERT_STATEMENT), taskExecution.getExecutionId(), jobExecution.getId());
}
/**
* The table prefix for the task batch table.
*
* @param tablePrefix defaults to {@link JdbcTaskExecutionDao#DEFAULT_TABLE_PREFIX}.
*/
public void setTablePrefix(String tablePrefix) {
Assert.notNull(tablePrefix, "Null is not allowed as a tablePrefix (use an empty string if you don't want a prefix at all).");
this.tablePrefix = tablePrefix;
}
private String getQuery(String base) {
return StringUtils.replace(base, "%PREFIX%", tablePrefix);
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2016 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.listener.support;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.springframework.batch.core.JobExecution;
import org.springframework.cloud.task.batch.listener.TaskBatchDao;
import org.springframework.cloud.task.repository.TaskExecution;
import org.springframework.util.Assert;
/**
* Map implementation of the {@link TaskBatchDao}. <note>This is intended for testing
* purposes only!</note>
*
* @author Michael Minella
*/
public class MapTaskBatchDao implements TaskBatchDao {
private Map<Long, Set<Long>> relationships;
public MapTaskBatchDao(Map<Long, Set<Long>> relationships) {
Assert.notNull(relationships, "Relationships must not be null");
this.relationships = relationships;
}
@Override
public void saveRelationship(TaskExecution taskExecution, JobExecution jobExecution) {
Assert.notNull(taskExecution, "A taskExecution is required");
Assert.notNull(jobExecution, "A jobExecution is required");
if(this.relationships.containsKey(taskExecution.getExecutionId())) {
this.relationships.get(taskExecution.getExecutionId()).add(jobExecution.getId());
}
else {
TreeSet<Long> jobExecutionIds = new TreeSet<>();
jobExecutionIds.add(jobExecution.getId());
this.relationships.put(taskExecution.getExecutionId(), jobExecutionIds);
}
}
}

View File

@@ -0,0 +1 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.springframework.cloud.task.batch.configuration.TaskBatchAutoConfiguration

View File

@@ -0,0 +1,189 @@
/*
* Copyright 2016 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.listener;
import java.util.Iterator;
import java.util.Set;
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.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration;
import org.springframework.cloud.task.batch.configuration.TaskBatchAutoConfiguration;
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.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.junit.Assert.assertEquals;
/**
* @author Michael Minella
*/
public class BatchTaskExecutionListenerTests {
private ConfigurableApplicationContext applicationContext;
@After
public void tearDown() {
if(this.applicationContext != null) {
this.applicationContext.close();
}
}
@Test
public void testAutobuiltDataSource() {
this.applicationContext = SpringApplication.run(new Object[] {JobConfiguration.class, PropertyPlaceholderAutoConfiguration.class, EmbeddedDataSourceConfiguration.class, TaskBatchAutoConfiguration.class, BatchAutoConfiguration.class, TaskBatchAutoConfiguration.class}, new String[0]);
TaskExplorer taskExplorer = this.applicationContext.getBean(TaskExplorer.class);
Page<TaskExecution> page = taskExplorer.findTaskExecutionsByName("application", new PageRequest(0, 1));
Set<Long> jobExecutionIds = taskExplorer.getJobExecutionIdsByTaskExecutionId(page.iterator().next().getExecutionId());
assertEquals(1, jobExecutionIds.size());
assertEquals(1, taskExplorer.getTaskExecution(jobExecutionIds.iterator().next()).getExecutionId());
}
@Test
public void testAutobuiltDataSourceNoJob() {
this.applicationContext = SpringApplication.run(new Object[] {NoJobConfiguration.class, PropertyPlaceholderAutoConfiguration.class, EmbeddedDataSourceConfiguration.class, TaskBatchAutoConfiguration.class, BatchAutoConfiguration.class, TaskBatchAutoConfiguration.class}, new String[0]);
TaskExplorer taskExplorer = this.applicationContext.getBean(TaskExplorer.class);
Page<TaskExecution> page = taskExplorer.findTaskExecutionsByName("application", new PageRequest(0, 1));
Set<Long> jobExecutionIds = taskExplorer.getJobExecutionIdsByTaskExecutionId(page.iterator().next().getExecutionId());
assertEquals(0, jobExecutionIds.size());
}
@Test
public void testMapBased() {
this.applicationContext = SpringApplication.run(new Object[] {JobConfiguration.class, PropertyPlaceholderAutoConfiguration.class, BatchAutoConfiguration.class, TaskBatchAutoConfiguration.class}, new String[0]);
TaskExplorer taskExplorer = this.applicationContext.getBean(TaskExplorer.class);
Page<TaskExecution> page = taskExplorer.findTaskExecutionsByName("application", new PageRequest(0, 1));
Set<Long> jobExecutionIds = taskExplorer.getJobExecutionIdsByTaskExecutionId(page.iterator().next().getExecutionId());
assertEquals(1, jobExecutionIds.size());
assertEquals(0, (long) taskExplorer.getTaskExecutionIdByJobExecutionId(jobExecutionIds.iterator().next()));
}
@Test
public void testMultipleJobs() {
this.applicationContext = SpringApplication.run(new Object[] {MultipleJobConfiguration.class, PropertyPlaceholderAutoConfiguration.class, BatchAutoConfiguration.class, TaskBatchAutoConfiguration.class}, new String[0]);
TaskExplorer taskExplorer = this.applicationContext.getBean(TaskExplorer.class);
Page<TaskExecution> page = taskExplorer.findTaskExecutionsByName("application", new PageRequest(0, 1));
Set<Long> jobExecutionIds = taskExplorer.getJobExecutionIdsByTaskExecutionId(page.iterator().next().getExecutionId());
assertEquals(2, jobExecutionIds.size());
Iterator<Long> jobExecutionIdsIterator = jobExecutionIds.iterator();
assertEquals(0, (long) taskExplorer.getTaskExecutionIdByJobExecutionId(jobExecutionIdsIterator.next()));
assertEquals(0, (long) taskExplorer.getTaskExecutionIdByJobExecutionId(jobExecutionIdsIterator.next()));
}
@Configuration
@EnableBatchProcessing
@EnableTask
public static class NoJobConfiguration {
}
@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 MultipleJobConfiguration {
@Autowired
private JobBuilderFactory jobBuilderFactory;
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Bean
public Job job1() {
return jobBuilderFactory.get("job1")
.start(stepBuilderFactory.get("job1step1").tasklet(new Tasklet() {
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
System.out.println("Executed job1");
return RepeatStatus.FINISHED;
}
}).build())
.build();
}
@Bean
public Job job2() {
return jobBuilderFactory.get("job2")
.start(stepBuilderFactory.get("job2step1").tasklet(new Tasklet() {
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
System.out.println("Executed job2");
return RepeatStatus.FINISHED;
}
}).build())
.build();
}
}
}