diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/SimpleTaskConfiguration.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/SimpleTaskConfiguration.java index ac60b298..36d121b4 100644 --- a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/SimpleTaskConfiguration.java +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/configuration/SimpleTaskConfiguration.java @@ -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(); diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/TaskException.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/TaskException.java new file mode 100644 index 00000000..e83e5284 --- /dev/null +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/TaskException.java @@ -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); + } + +} diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/TaskExecutionException.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/TaskExecutionException.java new file mode 100644 index 00000000..c4737986 --- /dev/null +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/TaskExecutionException.java @@ -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); + } +} diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/TaskExecutionListener.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/TaskExecutionListener.java index 014accdd..6969d228 100644 --- a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/TaskExecutionListener.java +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/TaskExecutionListener.java @@ -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); - } diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/TaskLifecycleListener.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/TaskLifecycleListener.java index 5fd6d162..fcde5233 100644 --- a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/TaskLifecycleListener.java +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/TaskLifecycleListener.java @@ -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{ @Autowired(required = false) - Collection taskExecutionListeners; + private Collection taskExecutionListeners; private final static Logger logger = LoggerFactory.getLogger(TaskLifecycleListener.class); diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/annotation/AfterTask.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/annotation/AfterTask.java new file mode 100644 index 00000000..546cd7f6 --- /dev/null +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/annotation/AfterTask.java @@ -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; + +/** + *

+ * {@link TaskExecutionListener#onTaskEnd(TaskExecution)} + *

+ * + *
+ * public class MyListener {
+
+ * 	@AfterTask
+ * 	public void  doSomething(TaskExecution taskExecution) {
+ * 	}
+ * }
+ * 
+ * + * @author Glenn Renfro + */ +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface AfterTask { +} diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/annotation/BeforeTask.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/annotation/BeforeTask.java new file mode 100644 index 00000000..3180ded2 --- /dev/null +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/annotation/BeforeTask.java @@ -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; + +/** + *

+ * {@link TaskExecutionListener#onTaskStartup(TaskExecution)} + *

+ * + *
+ * public class MyListener {
+
+ * 	@BeforeTask
+ * 	public void  doSomething(TaskExecution taskExecution) {
+ * 	}
+ * }
+ * 
+ * + * @author Glenn Renfro + */ +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface BeforeTask { +} diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/annotation/FailedTask.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/annotation/FailedTask.java new file mode 100644 index 00000000..62b7534f --- /dev/null +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/annotation/FailedTask.java @@ -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; + +/** + *

+ * {@link TaskExecutionListener#onTaskFailed(TaskExecution, Throwable)} + *

+ * + *
+ * public class MyListener {
+
+ * 	@FailedTask
+ * 	public void  doSomething(TaskExecution taskExecution, Throwable throwable) {
+ * 	}
+ * }
+ * 
+ * + * @author Glenn Renfro + */ +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface FailedTask { +} diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/annotation/TaskListenerExecutor.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/annotation/TaskListenerExecutor.java new file mode 100644 index 00000000..0d962301 --- /dev/null +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/annotation/TaskListenerExecutor.java @@ -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 beforeTaskInstances; + + private Map afterTaskInstances; + + private Map failedTaskInstances; + + public TaskListenerExecutor(Map beforeTaskInstances, + Map afterTaskInstances, + Map failedTaskInstances){ + + this.beforeTaskInstances = beforeTaskInstances; + this.afterTaskInstances = afterTaskInstances; + this.failedTaskInstances = failedTaskInstances; + } + + /** + * Executes all the methods that have been annotated with @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 @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 @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 methods, Map 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 methods, Map 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); + } + } + } + +} diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/annotation/TaskListenerExecutorFactory.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/annotation/TaskListenerExecutorFactory.java new file mode 100644 index 00000000..d8e075ba --- /dev/null +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/annotation/TaskListenerExecutorFactory.java @@ -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 { + + private final static Logger logger = LoggerFactory.getLogger(TaskListenerExecutor.class); + + private final Set> nonAnnotatedClasses = + Collections.newSetFromMap(new ConcurrentHashMap, Boolean>()); + + private ConfigurableApplicationContext context; + + private Map beforeTaskInstances; + + private Map afterTaskInstances; + + private Map 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 beforeTaskMethods = + (new MethodGetter()).getMethods(type, BeforeTask.class); + Map afterTaskMethods = + (new MethodGetter()).getMethods(type, AfterTask.class); + Map failedTaskMethods = + (new MethodGetter()).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 { + public Map getMethods(final Class type, final Class annotationClass){ + return MethodIntrospector.selectMethods(type, + new MethodIntrospector.MetadataLookup() { + @Override + public T inspect(Method method) { + return AnnotationUtils.findAnnotation(method, annotationClass); + } + }); + } + } + +} diff --git a/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/listener/TaskExecutionListenerTests.java b/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/listener/TaskExecutionListenerTests.java index c380c822..c292d8a8 100644 --- a/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/listener/TaskExecutionListenerTests.java +++ b/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/listener/TaskExecutionListenerTests.java @@ -25,15 +25,22 @@ import java.util.ArrayList; import java.util.Date; import org.junit.After; -import org.junit.Before; import org.junit.Test; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.context.event.ApplicationFailedEvent; +import org.springframework.cloud.task.listener.annotation.AfterTask; +import org.springframework.cloud.task.listener.annotation.BeforeTask; +import org.springframework.cloud.task.listener.annotation.FailedTask; +import org.springframework.cloud.task.listener.annotation.TaskListenerExecutor; +import org.springframework.cloud.task.listener.annotation.TaskListenerExecutorFactory; import org.springframework.cloud.task.repository.TaskExecution; import org.springframework.cloud.task.util.TestDefaultConfiguration; -import org.springframework.cloud.task.util.TestDefaultListenerConfiguration; +import org.springframework.cloud.task.util.TestListener; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.context.event.ContextClosedEvent; /** @@ -47,35 +54,36 @@ public class TaskExecutionListenerTests { private static final String EXCEPTION_MESSAGE = "This was expected"; - @Before - public void setUp() { - context = new AnnotationConfigApplicationContext(); - context.setId("testTask"); - context.register(TestDefaultListenerConfiguration.class, - TestDefaultConfiguration.class, - PropertyPlaceholderAutoConfiguration.class); - } - @After public void tearDown() { - context.close(); + if(context != null) { + context.close(); + } } + /** + * Verify that if a TaskExecutionListener Bean is present that the onTaskStartup method + * is called. + */ @Test public void testTaskCreate() { - context.refresh(); - TestDefaultListenerConfiguration.TestTaskExecutionListener taskExecutionListener = - context.getBean(TestDefaultListenerConfiguration.TestTaskExecutionListener.class); + setupContextForTaskExecutionListener(); + DefaultTaskListenerConfiguration.TestTaskExecutionListener taskExecutionListener = + context.getBean(DefaultTaskListenerConfiguration.TestTaskExecutionListener.class); TaskExecution taskExecution = new TaskExecution(0, null, "wombat", new Date(), new Date(), null, new ArrayList()); verifyListenerResults(true, false, false, taskExecution,taskExecutionListener); } + /** + * Verify that if a TaskExecutionListener Bean is present that the onTaskEnd method + * is called. + */ @Test public void testTaskUpdate() { - context.refresh(); - TestDefaultListenerConfiguration.TestTaskExecutionListener taskExecutionListener = - context.getBean(TestDefaultListenerConfiguration.TestTaskExecutionListener.class); + setupContextForTaskExecutionListener(); + DefaultTaskListenerConfiguration.TestTaskExecutionListener taskExecutionListener = + context.getBean(DefaultTaskListenerConfiguration.TestTaskExecutionListener.class); context.publishEvent(new ContextClosedEvent(context)); TaskExecution taskExecution = new TaskExecution(0, 0, "wombat", @@ -83,41 +91,184 @@ public class TaskExecutionListenerTests { verifyListenerResults(true, true, false, taskExecution,taskExecutionListener); } + /** + * Verify that if a TaskExecutionListener Bean is present that the onTaskFailed method + * is called. + */ @Test public void testTaskFail() { RuntimeException exception = new RuntimeException(EXCEPTION_MESSAGE); - context.refresh(); + setupContextForTaskExecutionListener(); context.publishEvent(new ApplicationFailedEvent(new SpringApplication(), new String[0], context, exception)); context.publishEvent(new ContextClosedEvent(context)); - TestDefaultListenerConfiguration.TestTaskExecutionListener taskExecutionListener = - context.getBean(TestDefaultListenerConfiguration.TestTaskExecutionListener.class); + DefaultTaskListenerConfiguration.TestTaskExecutionListener taskExecutionListener = + context.getBean(DefaultTaskListenerConfiguration.TestTaskExecutionListener.class); TaskExecution taskExecution = new TaskExecution(0, 1, "wombat", new Date(), new Date(), null, new ArrayList()); verifyListenerResults(true, true, true, taskExecution,taskExecutionListener); } + /** + * Verify that if a bean has a @BeforeTask annotation present that the associated + * method is called. + */ + @Test + public void testAnnotationCreate() throws Exception { + setupContextForAnnotatedListener(); + DefaultAnnotationConfiguration.AnnotatedTaskListener annotatedListener = + context.getBean(DefaultAnnotationConfiguration.AnnotatedTaskListener.class); + TaskExecution taskExecution = new TaskExecution(0, null, "wombat", + new Date(), new Date(), null, new ArrayList()); + verifyListenerResults(true, false, false, taskExecution,annotatedListener); + } + + /** + * Verify that if a bean has a @AfterTask annotation present that the associated + * method is called. + */ + @Test + public void testAnnotationUpdate() { + setupContextForAnnotatedListener(); + DefaultAnnotationConfiguration.AnnotatedTaskListener annotatedListener = + context.getBean(DefaultAnnotationConfiguration.AnnotatedTaskListener.class); + context.publishEvent(new ContextClosedEvent(context)); + + TaskExecution taskExecution = new TaskExecution(0, 0, "wombat", + new Date(), new Date(), null, new ArrayList()); + verifyListenerResults(true, true, false, taskExecution,annotatedListener); + } + + /** + * Verify that if a bean has a @FailedTask annotation present that the associated + * method is called. + */ + @Test + public void testAnnotationFail() { + RuntimeException exception = new RuntimeException(EXCEPTION_MESSAGE); + setupContextForAnnotatedListener(); + context.publishEvent(new ApplicationFailedEvent(new SpringApplication(), new String[0], context, exception)); + context.publishEvent(new ContextClosedEvent(context)); + DefaultAnnotationConfiguration.AnnotatedTaskListener annotatedListener = + context.getBean(DefaultAnnotationConfiguration.AnnotatedTaskListener.class); + + TaskExecution taskExecution = new TaskExecution(0, 1, "wombat", new Date(), + new Date(), null, new ArrayList()); + verifyListenerResults(true, true, true, taskExecution,annotatedListener); + } + private void verifyListenerResults (boolean isTaskStartup, boolean isTaskEnd, boolean isTaskFailed, TaskExecution taskExecution, - TestDefaultListenerConfiguration.TestTaskExecutionListener actualListener){ + TestListener actualListener){ assertEquals(isTaskStartup,actualListener.isTaskStartup()); assertEquals(isTaskEnd,actualListener.isTaskEnd()); assertEquals(isTaskFailed,actualListener.isTaskFailed()); if(isTaskFailed){ - assertEquals(TestDefaultListenerConfiguration.TestTaskExecutionListener.END_MESSAGE, actualListener.getTaskExecution().getExitMessage()); + assertEquals(TestListener.END_MESSAGE, actualListener.getTaskExecution().getExitMessage()); assertNotNull(actualListener.getThrowable()); assertTrue(actualListener.getThrowable() instanceof RuntimeException); } else if(isTaskEnd){ - assertEquals(TestDefaultListenerConfiguration.TestTaskExecutionListener.END_MESSAGE, actualListener.getTaskExecution().getExitMessage()); + assertEquals(TestListener.END_MESSAGE, actualListener.getTaskExecution().getExitMessage()); assertNull(actualListener.getThrowable()); } else { - assertEquals(TestDefaultListenerConfiguration.TestTaskExecutionListener.START_MESSAGE, actualListener.getTaskExecution().getExitMessage()); + assertEquals(TestListener.START_MESSAGE, actualListener.getTaskExecution().getExitMessage()); assertNull(actualListener.getThrowable()); } assertEquals(taskExecution.getExecutionId(), actualListener.getTaskExecution().getExecutionId()); assertEquals(taskExecution.getExitCode(), actualListener.getTaskExecution().getExitCode()); } + + private void setupContextForTaskExecutionListener(){ + context = new AnnotationConfigApplicationContext(DefaultTaskListenerConfiguration.class, + TestDefaultConfiguration.class, + PropertyPlaceholderAutoConfiguration.class); + context.setId("testTask"); + } + + private void setupContextForAnnotatedListener(){ + context = new AnnotationConfigApplicationContext(TestDefaultConfiguration.class, DefaultAnnotationConfiguration.class, + PropertyPlaceholderAutoConfiguration.class); + context.setId("annotatedTask"); + } + + @Configuration + public static class DefaultAnnotationConfiguration { + + @Bean + public AnnotatedTaskListener annotatedTaskListener() { + return new AnnotatedTaskListener(); + } + + @Bean + public TaskListenerExecutor taskListenerExecutor(ConfigurableApplicationContext context) throws Exception + { + TaskListenerExecutorFactory taskListenerExecutorFactory = new TaskListenerExecutorFactory(context); + return taskListenerExecutorFactory.getObject(); + } + + public static class AnnotatedTaskListener extends TestListener { + + @BeforeTask + public void methodA(TaskExecution taskExecution) { + isTaskStartup = true; + this.taskExecution = taskExecution; + this.taskExecution.setExitMessage(START_MESSAGE); + } + + @AfterTask + public void methodB(TaskExecution taskExecution) { + isTaskEnd = true; + this.taskExecution = taskExecution; + this.taskExecution.setExitMessage(END_MESSAGE); + + } + + @FailedTask + public void methodC(TaskExecution taskExecution, Throwable throwable) { + isTaskFailed = true; + this.taskExecution = taskExecution; + this.throwable = throwable; + this.taskExecution.setExitMessage(ERROR_MESSAGE); + } + } + } + + @Configuration + public static class DefaultTaskListenerConfiguration { + + @Bean + public TestTaskExecutionListener taskExecutionListener() { + return new TestTaskExecutionListener(); + } + + public static class TestTaskExecutionListener extends TestListener implements TaskExecutionListener { + + @Override + public void onTaskStartup(TaskExecution taskExecution) { + isTaskStartup = true; + this.taskExecution = taskExecution; + this.taskExecution.setExitMessage(START_MESSAGE); + } + + @Override + public void onTaskEnd(TaskExecution taskExecution) { + isTaskEnd = true; + this.taskExecution = taskExecution; + this.taskExecution.setExitMessage(END_MESSAGE); + } + + @Override + public void onTaskFailed(TaskExecution taskExecution, Throwable throwable) { + isTaskFailed = true; + this.taskExecution = taskExecution; + this.throwable = throwable; + this.taskExecution.setExitMessage(ERROR_MESSAGE); + } + } + } + } + diff --git a/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/util/TestDefaultListenerConfiguration.java b/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/util/TestDefaultListenerConfiguration.java deleted file mode 100644 index bb7e9ba5..00000000 --- a/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/util/TestDefaultListenerConfiguration.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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.util; - -import org.springframework.cloud.task.listener.TaskExecutionListener; -import org.springframework.cloud.task.repository.TaskExecution; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Initializes the beans needed to TestExecutionListenerTests. - * - * @author Glenn Renfro - */ -@Configuration -public class TestDefaultListenerConfiguration { - - @Bean - public TestTaskExecutionListener taskExecutionListener() { - return new TestTaskExecutionListener(); - } - - - public class TestTaskExecutionListener implements TaskExecutionListener { - - public static final String START_MESSAGE = "FOO"; - public static final String ERROR_MESSAGE = "BAR"; - public static final String END_MESSAGE = "BAZ"; - - private boolean isTaskStartup; - private boolean isTaskEnd; - private boolean isTaskFailed; - private TaskExecution taskExecution; - private Throwable throwable; - - @Override - public void onTaskStartup(TaskExecution taskExecution) { - isTaskStartup = true; - this.taskExecution = taskExecution; - this.taskExecution.setExitMessage(START_MESSAGE); - } - - @Override - public void onTaskEnd(TaskExecution taskExecution) { - isTaskEnd = true; - this.taskExecution = taskExecution; - this.taskExecution.setExitMessage(END_MESSAGE); - } - - @Override - public void onTaskFailed(TaskExecution taskExecution, Throwable throwable) { - isTaskFailed = true; - this.taskExecution = taskExecution; - this.throwable = throwable; - this.taskExecution.setExitMessage(ERROR_MESSAGE); - } - - public boolean isTaskStartup() { - return isTaskStartup; - } - - public boolean isTaskEnd() { - return isTaskEnd; - } - - public boolean isTaskFailed() { - return isTaskFailed; - } - - public TaskExecution getTaskExecution() { - return this.taskExecution; - } - - public Throwable getThrowable(){ - return throwable; - } - - } -} diff --git a/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/util/TestListener.java b/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/util/TestListener.java new file mode 100644 index 00000000..8062026f --- /dev/null +++ b/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/util/TestListener.java @@ -0,0 +1,75 @@ +/* + * 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.util; + +import org.springframework.cloud.task.repository.TaskExecution; + +/** + * Provides the basic infrastructure for evaluating if task listener performed + * properly. + * + * @author Glenn Renfro + */ +public abstract class TestListener { + public static final String START_MESSAGE = "FOO"; + public static final String ERROR_MESSAGE = "BAR"; + public static final String END_MESSAGE = "BAZ"; + + protected boolean isTaskStartup; + protected boolean isTaskEnd; + protected boolean isTaskFailed; + protected TaskExecution taskExecution; + protected Throwable throwable; + + /** + * Indicates if the task listener was called during task create step. + * @return true if task listener was called during task creation, else false. + */ + public boolean isTaskStartup() { + return isTaskStartup; + } + + /** + * Indicates if the task listener was called during task end. + * @return true if the task listener was called during task end, else false. + */ + public boolean isTaskEnd() { + return isTaskEnd; + } + + /** + * Indicates if the task listener was called during task failed step. + * @return true if task listener was called during task failure, else false. + */ + public boolean isTaskFailed() { + return isTaskFailed; + } + + /** + * Task Execution that was updated during listener call. + */ + public TaskExecution getTaskExecution() { + return taskExecution; + } + + /** + * The throwable that was sent with the task if task failed. + */ + public Throwable getThrowable() { + return throwable; + } +} diff --git a/spring-cloud-task-docs/src/main/asciidoc/features.adoc b/spring-cloud-task-docs/src/main/asciidoc/features.adoc index 1a8b8a2b..974938b6 100644 --- a/spring-cloud-task-docs/src/main/asciidoc/features.adoc +++ b/spring-cloud-task-docs/src/main/asciidoc/features.adoc @@ -185,3 +185,30 @@ notified for the following events: marking the final state of the task. . `onTaskFailed` - prior to the `onTaskEnd` method being invoked when an unhandled exception is thrown by the task. + +Spring Cloud Task also allows a user add `TaskExecution` Listeners to methods within a bean +by using the following method annotations: + +. `@BeforeTask` - prior to the storing the TaskExecution into the TaskRepository +. `@AfterTask` - prior to the updating of the TaskExecution entry in the TaskRepository +marking the final state of the task. +. `@FailedTask` - prior to the `@AfterTask` method being invoked when an unhandled + exception is thrown by the task. + +``` + public class MyBean { + + @BeforeTask + public void methodA(TaskExecution taskExecution) { + } + + @AfterTask + public void methodB(TaskExecution taskExecution) { + } + + @FailedTask + public void methodC(TaskExecution taskExecution, Throwable throwable) { + } +} +``` +