SCT-95 Support TaskExecutionListener Annotations

Added the ability to implement TaskExecutionListener functionality without
implementing the TaskExecutionListener interface.

resolves spring-cloud/spring-cloud-task#95
This commit is contained in:
Glenn Renfro
2016-02-23 10:44:54 -05:00
committed by Michael Minella
parent 20a56c9581
commit f486050086
14 changed files with 783 additions and 120 deletions

View File

@@ -27,6 +27,8 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.cloud.task.listener.TaskLifecycleListener;
import org.springframework.cloud.task.listener.annotation.TaskListenerExecutor;
import org.springframework.cloud.task.listener.annotation.TaskListenerExecutorFactory;
import org.springframework.cloud.task.repository.TaskExplorer;
import org.springframework.cloud.task.repository.TaskNameResolver;
import org.springframework.cloud.task.repository.TaskRepository;
@@ -72,6 +74,14 @@ public class SimpleTaskConfiguration {
return new TaskLifecycleListener(taskRepository(), taskNameResolver(), this.applicationArguments);
}
@Bean
public TaskListenerExecutor taskListenerExecutor(ConfigurableApplicationContext context) throws Exception
{
TaskListenerExecutorFactory taskListenerExecutorFactory =
new TaskListenerExecutorFactory(context);
return taskListenerExecutorFactory.getObject();
}
@Bean
public PlatformTransactionManager transactionManager() {
return this.configurer.getTransactionManager();

View File

@@ -0,0 +1,33 @@
/*
* 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.listener;
/**
* Base Exception for any Task issues.
* @author Glenn Renfro
*/
public class TaskException extends RuntimeException {
public TaskException(String message, Throwable e){
super(message, e);
}
public TaskException(String message){
super(message);
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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.listener;
/**
* Is thrown when executing a task.
*
* @author Glenn Renfro.
*/
public class TaskExecutionException extends TaskException {
public TaskExecutionException(String message){
super(message);
}
public TaskExecutionException(String message, Throwable throwable){
super(message, throwable);
}
}

View File

@@ -46,5 +46,4 @@ public interface TaskExecutionListener {
* @param throwable the uncaught exception that was thrown during task execution.
*/
public void onTaskFailed(TaskExecution taskExecution, Throwable throwable);
}

View File

@@ -26,6 +26,7 @@ import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ExitCodeEvent;
@@ -60,7 +61,7 @@ import org.springframework.util.Assert;
public class TaskLifecycleListener implements ApplicationListener<ApplicationEvent>{
@Autowired(required = false)
Collection<TaskExecutionListener> taskExecutionListeners;
private Collection<TaskExecutionListener> taskExecutionListeners;
private final static Logger logger = LoggerFactory.getLogger(TaskLifecycleListener.class);

View File

@@ -0,0 +1,48 @@
/*
* 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.listener.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.cloud.task.listener.TaskExecutionListener;
import org.springframework.cloud.task.repository.TaskExecution;
/**
* <p>
* {@link TaskExecutionListener#onTaskEnd(TaskExecution)}
* </p>
*
* <pre class="code">
* public class MyListener {
* &#064;AfterTask
* public void doSomething(TaskExecution taskExecution) {
* }
* }
* </pre>
*
* @author Glenn Renfro
*/
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AfterTask {
}

View File

@@ -0,0 +1,48 @@
/*
* 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.listener.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.cloud.task.listener.TaskExecutionListener;
import org.springframework.cloud.task.repository.TaskExecution;
/**
* <p>
* {@link TaskExecutionListener#onTaskStartup(TaskExecution)}
* </p>
*
* <pre class="code">
* public class MyListener {
* &#064;BeforeTask
* public void doSomething(TaskExecution taskExecution) {
* }
* }
* </pre>
*
* @author Glenn Renfro
*/
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BeforeTask {
}

View File

@@ -0,0 +1,48 @@
/*
* 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.listener.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.cloud.task.listener.TaskExecutionListener;
import org.springframework.cloud.task.repository.TaskExecution;
/**
* <p>
* {@link TaskExecutionListener#onTaskFailed(TaskExecution, Throwable)}
* </p>
*
* <pre class="code">
* public class MyListener {
* &#064;FailedTask
* public void doSomething(TaskExecution taskExecution, Throwable throwable) {
* }
* }
* </pre>
*
* @author Glenn Renfro
*/
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FailedTask {
}

View File

@@ -0,0 +1,119 @@
/*
* 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.listener.annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Set;
import org.springframework.cloud.task.listener.TaskExecutionException;
import org.springframework.cloud.task.listener.TaskExecutionListener;
import org.springframework.cloud.task.repository.TaskExecution;
/**
* Identifies all beans that contain a TaskExecutionListener annotation and stores the
* associated method so that it can be called by the {@link TaskExecutionListener} at the
* appropriate time.
*
* @author Glenn Renfro
*/
public class TaskListenerExecutor implements TaskExecutionListener{
private Map<Method, Object> beforeTaskInstances;
private Map<Method, Object> afterTaskInstances;
private Map<Method, Object> failedTaskInstances;
public TaskListenerExecutor(Map<Method, Object> beforeTaskInstances,
Map<Method, Object> afterTaskInstances,
Map<Method, Object> failedTaskInstances){
this.beforeTaskInstances = beforeTaskInstances;
this.afterTaskInstances = afterTaskInstances;
this.failedTaskInstances = failedTaskInstances;
}
/**
* Executes all the methods that have been annotated with &#064;BeforeTask.
* @param taskExecution associated with the event.
*/
@Override
public void onTaskStartup(TaskExecution taskExecution) {
executeTaskListener(taskExecution, beforeTaskInstances.keySet(), beforeTaskInstances);
}
/**
* Executes all the methods that have been annotated with &#064;AfterTask.
* @param taskExecution associated with the event.
*/
@Override
public void onTaskEnd(TaskExecution taskExecution) {
executeTaskListener(taskExecution, afterTaskInstances.keySet(), afterTaskInstances);
}
/**
* Executes all the methods that have been annotated with &#064;FailedTask.
* @param throwable that was not caught for the task execution.
* @param taskExecution associated with the event.
*/
@Override
public void onTaskFailed(TaskExecution taskExecution, Throwable throwable) {
executeTaskListenerWithThrowable(taskExecution, throwable,
failedTaskInstances.keySet(),failedTaskInstances);
}
private void executeTaskListener(TaskExecution taskExecution, Set<Method> methods, Map<Method, Object> instances){
for (Method method : methods) {
try {
method.invoke(instances.get(method),taskExecution);
}
catch (IllegalAccessException e) {
throw new TaskExecutionException("@BeforeTask and @AfterTask annotated methods must be public.", e);
}
catch (InvocationTargetException e) {
throw new TaskExecutionException("Failed to process @BeforeTask or @AfterTask" +
"annotation because: ", e);
}
catch (IllegalArgumentException e){
throw new TaskExecutionException("taskExecution parameter is required for @BeforeTask and @AfterTask annotated methods", e);
}
}
}
private void executeTaskListenerWithThrowable(TaskExecution taskExecution,
Throwable throwable, Set<Method> methods, Map<Method, Object> instances){
for (Method method : methods) {
try {
method.invoke(instances.get(method),taskExecution, throwable);
}
catch (IllegalAccessException e) {
throw new TaskExecutionException("@FailedTask annotated methods must be public.", e);
}
catch (InvocationTargetException e) {
throw new TaskExecutionException("Failed to process @FailedTask " +
"annotation because: ", e);
}
catch (IllegalArgumentException e){
throw new TaskExecutionException("taskExecution and throwable parameters "
+ "are required for @FailedTask annotated methods", e);
}
}
}
}

View File

@@ -0,0 +1,164 @@
/*
* 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.listener.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
import org.springframework.aop.scope.ScopedObject;
import org.springframework.aop.scope.ScopedProxyUtils;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.annotation.AnnotationUtils;
/**
* @author Glenn Renfro
*/
public class TaskListenerExecutorFactory implements FactoryBean<TaskListenerExecutor> {
private final static Logger logger = LoggerFactory.getLogger(TaskListenerExecutor.class);
private final Set<Class<?>> nonAnnotatedClasses =
Collections.newSetFromMap(new ConcurrentHashMap<Class<?>, Boolean>());
private ConfigurableApplicationContext context;
private Map<Method, Object> beforeTaskInstances;
private Map<Method, Object> afterTaskInstances;
private Map<Method, Object> failedTaskInstances;
public TaskListenerExecutorFactory(ConfigurableApplicationContext context){
this.context = context;
}
@Override
public TaskListenerExecutor getObject() throws Exception {
beforeTaskInstances = new HashMap<>();
afterTaskInstances = new HashMap<>();
failedTaskInstances = new HashMap<>();
initializeExecutor();
return new TaskListenerExecutor(beforeTaskInstances, afterTaskInstances, failedTaskInstances);
}
@Override
public Class<?> getObjectType() {
return TaskListenerExecutor.class;
}
@Override
public boolean isSingleton() {
return false;
}
private void initializeExecutor( ) {
ConfigurableListableBeanFactory factory = context.getBeanFactory();
for( String beanName : context.getBeanDefinitionNames()) {
if (!ScopedProxyUtils.isScopedTarget(beanName)) {
Class<?> type = null;
try {
type = AutoProxyUtils.determineTargetClass(factory, beanName);
}
catch (RuntimeException ex) {
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
if (logger.isDebugEnabled()) {
logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
}
}
if (type != null) {
if (ScopedObject.class.isAssignableFrom(type)) {
try {
type = AutoProxyUtils.determineTargetClass(factory,
ScopedProxyUtils.getTargetBeanName(beanName));
}
catch (RuntimeException ex) {
// An invalid scoped proxy arrangement - let's ignore it.
if (logger.isDebugEnabled()) {
logger.debug("Could not resolve target bean for scoped proxy '" + beanName + "'", ex);
}
}
}
try {
processBean(beanName, type);
}
catch (RuntimeException ex) {
throw new BeanInitializationException("Failed to process @BeforeTask " +
"annotation on bean with name '" + beanName + "'", ex);
}
}
}
}
}
private void processBean(String beanName, final Class<?> type){
if (!this.nonAnnotatedClasses.contains(type)) {
Map<Method, BeforeTask> beforeTaskMethods =
(new MethodGetter<BeforeTask>()).getMethods(type, BeforeTask.class);
Map<Method, AfterTask> afterTaskMethods =
(new MethodGetter<AfterTask>()).getMethods(type, AfterTask.class);
Map<Method, FailedTask> failedTaskMethods =
(new MethodGetter<FailedTask>()).getMethods(type, FailedTask.class);
if (beforeTaskMethods.isEmpty() && afterTaskMethods.isEmpty()) {
this.nonAnnotatedClasses.add(type);
return;
}
if(!beforeTaskMethods.isEmpty()) {
for(Method beforeTaskMethod : beforeTaskMethods.keySet()) {
this.beforeTaskInstances.put(beforeTaskMethod, context.getBean(beanName));
}
}
if(!afterTaskMethods.isEmpty()){
for(Method afterTaskMethod : afterTaskMethods.keySet()) {
this.afterTaskInstances.put(afterTaskMethod, context.getBean(beanName));
}
}
if(!failedTaskMethods.isEmpty()){
for(Method failedTaskMethod : failedTaskMethods.keySet()) {
this.failedTaskInstances.put(failedTaskMethod, context.getBean(beanName));
}
}
}
}
private static class MethodGetter<T extends Annotation> {
public Map<Method, T> getMethods(final Class<?> type, final Class<T> annotationClass){
return MethodIntrospector.selectMethods(type,
new MethodIntrospector.MetadataLookup<T>() {
@Override
public T inspect(Method method) {
return AnnotationUtils.findAnnotation(method, annotationClass);
}
});
}
}
}