Refactor of Task API

This change removes the Spring Cloud Task dependency upon a
CommandLineRunner from boot and moves the handling of the task lifecycle
closer to the "edge" of a Spring Boot application.

With this change, now a developer simply adds @EnableTask to their
configuration somewhere and the task lifecycle will be recorded.

Resolves spring-cloud/spring-cloud-task#39
This commit is contained in:
Michael Minella
2015-12-15 13:00:42 -06:00
committed by Glenn Renfro
parent 7be3a88826
commit 61d3cc3641
23 changed files with 453 additions and 465 deletions

View File

@@ -1,44 +0,0 @@
/*
* Copyright 2015 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.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation that identifies a class as a task. This annotation will serve as the
* main “hook” to activate the various Spring Cloud Task features.
*
* @author Glenn Renfro
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Task {
/**
* Establishes the name associated with the task. The default is empty.
* @return returns name associated with the task or an empty string.
*/
public String value() default "";
}

View File

@@ -1,4 +0,0 @@
/**
* Annotations for spring cloud task.
*/
package org.springframework.cloud.task.annotation;

View File

@@ -20,6 +20,8 @@ import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.support.transaction.ResourcelessTransactionManager;
import org.springframework.cloud.task.repository.TaskExplorer;
import org.springframework.cloud.task.repository.TaskRepository;
import org.springframework.cloud.task.repository.dao.JdbcTaskExecutionDao;
@@ -29,6 +31,8 @@ import org.springframework.cloud.task.repository.support.JdbcTaskRepositoryFacto
import org.springframework.cloud.task.repository.support.MapTaskExplorerFactoryBean;
import org.springframework.cloud.task.repository.support.MapTaskRepositoryFactoryBean;
import org.springframework.cloud.task.repository.support.SimpleTaskRepository;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
/**
* Default implementation of the TaskConfigurer interface. If no {@link TaskConfigurer}
@@ -52,6 +56,8 @@ public class DefaultTaskConfigurer implements TaskConfigurer {
private TaskExplorer taskExplorer;
private PlatformTransactionManager transactionManager;
public DefaultTaskConfigurer(){
initialize();
}
@@ -61,14 +67,21 @@ public class DefaultTaskConfigurer implements TaskConfigurer {
initialize();
}
@Override
public TaskRepository getTaskRepository() {
return taskRepository;
}
@Override
public TaskExplorer getTaskExplorer() {
return taskExplorer;
}
@Override
public PlatformTransactionManager getTransactionManager() {
return this.transactionManager;
}
private void initialize(){
logger.debug("Initializing TaskRepository");
if (dataSource == null) {
@@ -78,6 +91,7 @@ public class DefaultTaskConfigurer implements TaskConfigurer {
MapTaskExplorerFactoryBean mapTaskExplorerFactoryBean =
new MapTaskExplorerFactoryBean();
taskExplorer = mapTaskExplorerFactoryBean.getObject();
transactionManager = new ResourcelessTransactionManager();
}
else {
@@ -87,6 +101,7 @@ public class DefaultTaskConfigurer implements TaskConfigurer {
JdbcTaskExplorerFactoryBean jdbcTaskExplorerFactoryBean =
new JdbcTaskExplorerFactoryBean(dataSource);
taskExplorer = jdbcTaskExplorerFactoryBean.getObject();
transactionManager = new DataSourceTransactionManager(dataSource);
}
}

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.springframework.cloud.task.annotation;
package org.springframework.cloud.task.configuration;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
@@ -24,16 +24,12 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.cloud.task.configuration.DefaultTaskConfigurer;
import org.springframework.cloud.task.configuration.SimpleTaskConfiguration;
import org.springframework.cloud.task.configuration.TaskConfigurer;
import org.springframework.cloud.task.repository.TaskRepository;
import org.springframework.context.annotation.Import;
/**
* <p>
* Enable Spring Task features and provide a base configuration for setting up
* {@link Task} objects in an &#064;Configuration class.
* Enable Spring Task features.
*
* <pre class="code">
* &#064;Configuration

View File

@@ -23,15 +23,17 @@ import javax.sql.DataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.task.listener.TaskLifecycleListener;
import org.springframework.cloud.task.repository.TaskRepository;
import org.springframework.cloud.task.repository.support.TaskDatabaseInitializer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.core.io.ResourceLoader;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
@@ -41,13 +43,12 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
*
* @author Glenn Renfro
*/
@EnableTransactionManagement
@Configuration
@EnableTransactionManagement
public class SimpleTaskConfiguration {
protected static final Log logger = LogFactory.getLog(SimpleTaskConfiguration.class);
@Autowired
private ApplicationContext context;
@@ -66,17 +67,23 @@ public class SimpleTaskConfiguration {
private TaskConfigurer configurer;
@Bean
@Scope("prototype")
public TaskHandler taskHandler() {
return new TaskHandler();
}
private PlatformTransactionManager transactionManager;
@Bean
public TaskRepository taskRepository(){
return taskRepository;
}
@Bean
public TaskLifecycleListener taskLifecycleListener() {
return new TaskLifecycleListener(taskRepository());
}
@Bean
public PlatformTransactionManager transactionManager() {
return this.transactionManager;
}
/**
* Sets up the basic components by extracting them from the {@link TaskConfigurer}, defaulting to some
* sensible values as long as a unique DataSource is available.
@@ -93,6 +100,7 @@ public class SimpleTaskConfiguration {
logger.debug(String.format("Using %s TaskConfigurer",
configurer.getClass().getName()));
taskRepository = configurer.getTaskRepository();
transactionManager = configurer.getTransactionManager();
initialized = true;
}

View File

@@ -16,7 +16,9 @@
package org.springframework.cloud.task.configuration;
import org.springframework.cloud.task.repository.TaskExplorer;
import org.springframework.cloud.task.repository.TaskRepository;
import org.springframework.transaction.PlatformTransactionManager;
/**
* Provides a strategy interface for providing configuration
@@ -33,4 +35,7 @@ public interface TaskConfigurer {
*/
public TaskRepository getTaskRepository();
public PlatformTransactionManager getTransactionManager();
public TaskExplorer getTaskExplorer();
}

View File

@@ -1,124 +0,0 @@
/*
* Copyright 2015 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.configuration;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.ExitCodeGenerator;
import org.springframework.cloud.task.annotation.Task;
import org.springframework.cloud.task.repository.TaskExecution;
import org.springframework.cloud.task.repository.TaskRepository;
import org.springframework.context.ApplicationContext;
/**
* Offers the advice on how to record tasks to the repository for both
* {@link org.springframework.boot.ApplicationRunner} and
* {@link org.springframework.boot.CommandLineRunner} spring boot applications.
*
* @author Glenn Renfro
*/
@Aspect
public class TaskHandler {
@Autowired
private ApplicationContext context;
@Autowired
private TaskRepository repository;
private String taskName;
private String executionId;
private TaskExecution taskExecution;
/**
* Looks for any {@link CommandLineRunner}.run method with its class annotated with @Task
* and calls the repository implementation to store the start of the task in the repo
* before the run starts.
*
* @param joinPoint the point where the run method in a {@link CommandLineRunner} or
* {@link ApplicationRunner} is executed
*/
@Before("within( @org.springframework.cloud.task.annotation.Task *) && (execution(* org.springframework.boot.CommandLineRunner.run(..)) || execution(* org.springframework.boot.ApplicationRunner.run(..)))")
public void beforeCommandLineRunner(JoinPoint joinPoint) {
executionId = UUID.randomUUID().toString();
taskExecution = new TaskExecution();
Task a = joinPoint.getTarget().getClass().getAnnotation(Task.class);
taskName = a.value();
if (taskName == null || taskName.length() == 0) {
taskName = joinPoint.getTarget().getClass().getName();
}
taskExecution.setTaskName(taskName);
taskExecution.setStartTime(new Date());
taskExecution.setExecutionId(executionId);
repository.createTaskExecution(taskExecution);
}
/**
* Looks for any {@link CommandLineRunner}.run method with its class annotated with @Task
* and calls repository implementation to store the exit of the task in the repo after
* run returns result.
*
* @param joinPoint the point where the run method in a {@link CommandLineRunner} or
* {@link ApplicationRunner} is executed
*/
@AfterReturning("within( @org.springframework.cloud.task.annotation.Task *) && (execution(* org.springframework.boot.CommandLineRunner.run(..)) || execution(* org.springframework.boot.ApplicationRunner.run(..)))")
public void afterReturnCommandLineRunner(JoinPoint joinPoint) {
int result = 0;
List<ExitCodeGenerator> generators = new ArrayList<ExitCodeGenerator>();
generators
.addAll(context.getBeansOfType(ExitCodeGenerator.class).values());
for (ExitCodeGenerator generator : generators) {
result = generator.getExitCode();
}
taskExecution.setEndTime(new Date());
taskExecution.setExitCode(result);
repository.update(taskExecution);
}
/**
* Looks for any {@link CommandLineRunner}.run method with its class annotated with @Task
* and calls the repository implementation to store the exitCode of 1
* for the task in the repo in the case of an exception.
*
* @param joinPoint the point where the run method in a {@link CommandLineRunner} or
* {@link ApplicationRunner} is executed
*/
@AfterThrowing("within( @org.springframework.cloud.task.annotation.Task *) && (execution(* org.springframework.boot.CommandLineRunner.run(..)) || execution(* org.springframework.boot.ApplicationRunner.run(..)))")
public void logExceptionCommandLineRunner(JoinPoint joinPoint) {
taskExecution.setEndTime(new Date());
taskExecution.setExitCode(1);
repository.update(taskExecution);
}
public TaskExecution getTaskExecution() {
return taskExecution;
}
}

View File

@@ -0,0 +1,170 @@
/*
* Copyright 2015 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.listener;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Date;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.boot.context.event.ApplicationFailedEvent;
import org.springframework.cloud.task.repository.TaskExecution;
import org.springframework.cloud.task.repository.TaskRepository;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextRefreshedEvent;
/**
* Monitors the lifecycle of a task. This listener will record both the start and end of
* a task in the registered {@link TaskRepository}.
*
* The following events are used to identify the start and end of a task:
*
* <ul>
* <li>{@link ContextRefreshedEvent} - Used to identify the start of a task. A task
* is expected to contain a single application context.</li>
* <li>{@link ContextClosedEvent} - Used to identify the successful end of a task.</li>
* <li>{@link ApplicationFailedEvent} - Used to identify the failure of a task.</li>
* </ul>
*
* <b>NOTE:</b> Multiple contexts (including parent/child relationships) will result in
* only the first context refresh that contains this instance being recorded.
*
* @author Michael Minella
*/
public class TaskLifecycleListener implements ApplicationListener<ApplicationEvent>,
ApplicationContextAware {
private final static Logger logger = LoggerFactory.getLogger(TaskLifecycleListener.class);
private final TaskRepository taskRepository;
private TaskExecution taskExecution;
private boolean started = false;
private ApplicationContext applicationContext;
/**
* @param taskRepository The repository to record executions in.
*/
public TaskLifecycleListener(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}
/**
* Utilizes {@link ApplicationEvent}s to determine the start, end, and failure of a
* task. Specifically:
* <ul>
* <li>{@link ContextRefreshedEvent} - Start of a task</li>
* <li>{@link ContextClosedEvent} - Successful end of a task</li>
* <li>{@link ApplicationFailedEvent} - Failure of a task</li>
* </ul>
*
* @param applicationEvent The application being listened for.
*/
@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
if(applicationEvent instanceof ContextRefreshedEvent) {
doTaskStart();
started = true;
}
else if(applicationEvent instanceof ContextClosedEvent) {
doTaskEnd();
}
else if(applicationEvent instanceof ApplicationFailedEvent) {
doTaskFailed(((ApplicationFailedEvent) applicationEvent).getException());
}
}
private void doTaskFailed(Throwable exception) {
if(started) {
this.taskExecution.setEndTime(new Date());
//TODO: get exit code from new event thrown by Boot.
this.taskExecution.setExitCode(1);
this.taskExecution.setExitMessage(stackTraceToString(exception));
taskRepository.update(taskExecution);
}
else {
logger.error("An event to fail a task has been received for a task that has " +
"not yet started.");
}
}
private String stackTraceToString(Throwable exception) {
StringWriter writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
exception.printStackTrace(printWriter);
return writer.toString();
}
private void doTaskEnd() {
if(started) {
this.taskExecution.setEndTime(new Date());
//TODO: get exit code from new event thrown by Boot.
this.taskExecution.setExitCode(0);
taskRepository.update(taskExecution);
}
else {
logger.error("An event to end a task has been received for a task that has " +
"not yet started.");
}
}
private void doTaskStart() {
if(!started) {
String executionId = UUID.randomUUID().toString();
this.taskExecution = new TaskExecution();
this.taskExecution.setTaskName(applicationContext.getId());
this.taskExecution.setStartTime(new Date());
this.taskExecution.setExecutionId(executionId);
this.taskRepository.createTaskExecution(this.taskExecution);
}
else {
logger.error("Multiple start events have been received. The first one was " +
"recorded.");
}
}
/**
* Used for testing purposes
* @return the current {@link TaskExecution}
*/
TaskExecution getTaskExecution() {
return this.taskExecution;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
}