From 8b4352095dcb71b66b60e7336875310ebaef9004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C5=9F=C4=B1k=20Erhan?= Date: Sun, 15 Aug 2021 14:58:24 +0300 Subject: [PATCH] Support multiple task listener beans of same type --- .../TaskListenerExecutorObjectFactory.java | 25 ++- .../annotation/TaskListenerExecutor.java | 92 +++++---- ...askListenerExecutorObjectFactoryTests.java | 184 +++++++++++++++--- 3 files changed, 220 insertions(+), 81 deletions(-) diff --git a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/TaskListenerExecutorObjectFactory.java b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/TaskListenerExecutorObjectFactory.java index 5092f680..1914260e 100644 --- a/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/TaskListenerExecutorObjectFactory.java +++ b/spring-cloud-task-core/src/main/java/org/springframework/cloud/task/listener/TaskListenerExecutorObjectFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2021 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. @@ -20,6 +20,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -45,6 +46,7 @@ import org.springframework.core.annotation.AnnotationUtils; * Initializes TaskListenerExecutor for a task. * * @author Glenn Renfro + * @author Isik Erhan * @since 2.1.0 */ public class TaskListenerExecutorObjectFactory @@ -57,11 +59,11 @@ public class TaskListenerExecutorObjectFactory private ConfigurableApplicationContext context; - private Map beforeTaskInstances; + private Map> beforeTaskInstances; - private Map afterTaskInstances; + private Map> afterTaskInstances; - private Map failedTaskInstances; + private Map> failedTaskInstances; public TaskListenerExecutorObjectFactory(ConfigurableApplicationContext context) { this.context = context; @@ -141,20 +143,23 @@ public class TaskListenerExecutorObjectFactory } if (!beforeTaskMethods.isEmpty()) { for (Method beforeTaskMethod : beforeTaskMethods.keySet()) { - this.beforeTaskInstances.put(beforeTaskMethod, - this.context.getBean(beanName)); + this.beforeTaskInstances + .computeIfAbsent(beforeTaskMethod, k -> new LinkedHashSet<>()) + .add(this.context.getBean(beanName)); } } if (!afterTaskMethods.isEmpty()) { for (Method afterTaskMethod : afterTaskMethods.keySet()) { - this.afterTaskInstances.put(afterTaskMethod, - this.context.getBean(beanName)); + this.afterTaskInstances + .computeIfAbsent(afterTaskMethod, k -> new LinkedHashSet<>()) + .add(this.context.getBean(beanName)); } } if (!failedTaskMethods.isEmpty()) { for (Method failedTaskMethod : failedTaskMethods.keySet()) { - this.failedTaskInstances.put(failedTaskMethod, - this.context.getBean(beanName)); + this.failedTaskInstances + .computeIfAbsent(failedTaskMethod, k -> new LinkedHashSet<>()) + .add(this.context.getBean(beanName)); } } } 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 index 3c3b9ed7..3d0da216 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2021 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. @@ -31,18 +31,19 @@ import org.springframework.cloud.task.repository.TaskExecution; * appropriate time. * * @author Glenn Renfro + * @author Isik Erhan */ public class TaskListenerExecutor implements TaskExecutionListener { - private Map beforeTaskInstances; + private Map> beforeTaskInstances; - private Map afterTaskInstances; + private Map> afterTaskInstances; - private Map failedTaskInstances; + private Map> failedTaskInstances; - public TaskListenerExecutor(Map beforeTaskInstances, - Map afterTaskInstances, - Map failedTaskInstances) { + public TaskListenerExecutor(Map> beforeTaskInstances, + Map> afterTaskInstances, + Map> failedTaskInstances) { this.beforeTaskInstances = beforeTaskInstances; this.afterTaskInstances = afterTaskInstances; @@ -81,48 +82,55 @@ public class TaskListenerExecutor implements TaskExecutionListener { } private void executeTaskListener(TaskExecution taskExecution, Set methods, - Map instances) { + 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(String.format( - "Failed to process @BeforeTask or @AfterTask" - + " annotation because: %s", - e.getTargetException().getMessage()), e); - } - catch (IllegalArgumentException e) { - throw new TaskExecutionException("taskExecution parameter " - + "is required for @BeforeTask and @AfterTask annotated methods", - e); + for (Object instance : instances.get(method)) { + try { + method.invoke(instance, taskExecution); + } + catch (IllegalAccessException e) { + throw new TaskExecutionException( + "@BeforeTask and @AfterTask annotated methods must be public.", + e); + } + catch (InvocationTargetException e) { + throw new TaskExecutionException(String.format( + "Failed to process @BeforeTask or @AfterTask" + + " annotation because: %s", + e.getTargetException().getMessage()), 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) { + 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(String.format( - "Failed to process @FailedTask " + "annotation because: %s", - e.getTargetException().getMessage()), e); - } - catch (IllegalArgumentException e) { - throw new TaskExecutionException("taskExecution and throwable parameters " - + "are required for @FailedTask annotated methods", e); + for (Object instance : instances.get(method)) { + try { + method.invoke(instance, taskExecution, throwable); + } + catch (IllegalAccessException e) { + throw new TaskExecutionException( + "@FailedTask annotated methods must be public.", e); + } + catch (InvocationTargetException e) { + throw new TaskExecutionException(String.format( + "Failed to process @FailedTask " + "annotation because: %s", + e.getTargetException().getMessage()), 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/test/java/org/springframework/cloud/task/listener/TaskListenerExecutorObjectFactoryTests.java b/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/listener/TaskListenerExecutorObjectFactoryTests.java index 71c93639..347bc2dd 100644 --- a/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/listener/TaskListenerExecutorObjectFactoryTests.java +++ b/spring-cloud-task-core/src/test/java/org/springframework/cloud/task/listener/TaskListenerExecutorObjectFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2021 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. @@ -19,11 +19,10 @@ package org.springframework.cloud.task.listener; import java.util.ArrayList; import java.util.List; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.task.listener.annotation.AfterTask; import org.springframework.cloud.task.listener.annotation.BeforeTask; import org.springframework.cloud.task.listener.annotation.FailedTask; @@ -43,6 +42,7 @@ import static org.assertj.core.api.Assertions.assertThat; * {@link TaskListenerExecutor}. * * @author Glenn Renfro + * @author Isik Erhan * @since 2.1.0 */ @ExtendWith(SpringExtension.class) @@ -71,55 +71,161 @@ public class TaskListenerExecutorObjectFactoryTests { */ public static List taskExecutionListenerResults = new ArrayList<>(3); - @Autowired - private ConfigurableApplicationContext context; - private TaskListenerExecutor taskListenerExecutor; private TaskListenerExecutorObjectFactory taskListenerExecutorObjectFactory; - @BeforeEach - public void setup() { + public void setup(ConfigurableApplicationContext context) { taskExecutionListenerResults.clear(); this.taskListenerExecutorObjectFactory = new TaskListenerExecutorObjectFactory( - this.context); + context); this.taskListenerExecutor = this.taskListenerExecutorObjectFactory.getObject(); } @Test public void verifyTaskStartupListener() { - this.taskListenerExecutor - .onTaskStartup(createSampleTaskExecution(BEFORE_LISTENER)); - validateSingleEntry(BEFORE_LISTENER); + ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner() + .withUserConfiguration(TaskExecutionListenerConfiguration.class); + + applicationContextRunner.run((context) -> { + setup(context); + + this.taskListenerExecutor + .onTaskStartup(createSampleTaskExecution(BEFORE_LISTENER)); + validateSingleEntry(BEFORE_LISTENER); + }); } @Test public void verifyTaskFailedListener() { - this.taskListenerExecutor.onTaskFailed(createSampleTaskExecution(FAIL_LISTENER), - new IllegalStateException("oops")); - validateSingleEntry(FAIL_LISTENER); + ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner() + .withUserConfiguration(TaskExecutionListenerConfiguration.class); + + applicationContextRunner.run((context) -> { + setup(context); + + this.taskListenerExecutor.onTaskFailed( + createSampleTaskExecution(FAIL_LISTENER), + new IllegalStateException("oops")); + validateSingleEntry(FAIL_LISTENER); + }); } @Test public void verifyTaskEndListener() { - this.taskListenerExecutor.onTaskEnd(createSampleTaskExecution(AFTER_LISTENER)); - validateSingleEntry(AFTER_LISTENER); + ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner() + .withUserConfiguration(TaskExecutionListenerConfiguration.class); + + applicationContextRunner.run((context) -> { + setup(context); + + this.taskListenerExecutor + .onTaskEnd(createSampleTaskExecution(AFTER_LISTENER)); + validateSingleEntry(AFTER_LISTENER); + }); } @Test public void verifyAllListener() { - this.taskListenerExecutor - .onTaskStartup(createSampleTaskExecution(BEFORE_LISTENER)); - this.taskListenerExecutor.onTaskFailed(createSampleTaskExecution(FAIL_LISTENER), - new IllegalStateException("oops")); - this.taskListenerExecutor.onTaskEnd(createSampleTaskExecution(AFTER_LISTENER)); - assertThat(taskExecutionListenerResults.size()).isEqualTo(3); - assertThat(taskExecutionListenerResults.get(0).getTaskName()) - .isEqualTo(BEFORE_LISTENER); - assertThat(taskExecutionListenerResults.get(1).getTaskName()) - .isEqualTo(FAIL_LISTENER); - assertThat(taskExecutionListenerResults.get(2).getTaskName()) - .isEqualTo(AFTER_LISTENER); + ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner() + .withUserConfiguration(TaskExecutionListenerConfiguration.class); + + applicationContextRunner.run((context) -> { + setup(context); + + this.taskListenerExecutor + .onTaskStartup(createSampleTaskExecution(BEFORE_LISTENER)); + this.taskListenerExecutor.onTaskFailed( + createSampleTaskExecution(FAIL_LISTENER), + new IllegalStateException("oops")); + this.taskListenerExecutor + .onTaskEnd(createSampleTaskExecution(AFTER_LISTENER)); + assertThat(taskExecutionListenerResults.size()).isEqualTo(3); + assertThat(taskExecutionListenerResults.get(0).getTaskName()) + .isEqualTo(BEFORE_LISTENER); + assertThat(taskExecutionListenerResults.get(1).getTaskName()) + .isEqualTo(FAIL_LISTENER); + assertThat(taskExecutionListenerResults.get(2).getTaskName()) + .isEqualTo(AFTER_LISTENER); + }); + } + + @Test + public void verifyTaskStartupListenerWithMultipleInstances() { + ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner() + .withUserConfiguration( + TaskExecutionListenerMultipleInstanceConfiguration.class); + + applicationContextRunner.run((context) -> { + setup(context); + + this.taskListenerExecutor + .onTaskStartup(createSampleTaskExecution(BEFORE_LISTENER)); + validateSingleEventWithMultipleInstances(BEFORE_LISTENER); + }); + } + + @Test + public void verifyTaskFailedListenerWithMultipleInstances() { + ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner() + .withUserConfiguration( + TaskExecutionListenerMultipleInstanceConfiguration.class); + + applicationContextRunner.run((context) -> { + setup(context); + + this.taskListenerExecutor.onTaskFailed( + createSampleTaskExecution(FAIL_LISTENER), + new IllegalStateException("oops")); + validateSingleEventWithMultipleInstances(FAIL_LISTENER); + }); + } + + @Test + public void verifyTaskEndListenerWithMultipleInstances() { + ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner() + .withUserConfiguration( + TaskExecutionListenerMultipleInstanceConfiguration.class); + + applicationContextRunner.run((context) -> { + setup(context); + + this.taskListenerExecutor + .onTaskEnd(createSampleTaskExecution(AFTER_LISTENER)); + validateSingleEventWithMultipleInstances(AFTER_LISTENER); + }); + } + + @Test + public void verifyAllListenerWithMultipleInstances() { + ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner() + .withUserConfiguration( + TaskExecutionListenerMultipleInstanceConfiguration.class); + + applicationContextRunner.run((context) -> { + setup(context); + + this.taskListenerExecutor + .onTaskStartup(createSampleTaskExecution(BEFORE_LISTENER)); + this.taskListenerExecutor.onTaskFailed( + createSampleTaskExecution(FAIL_LISTENER), + new IllegalStateException("oops")); + this.taskListenerExecutor + .onTaskEnd(createSampleTaskExecution(AFTER_LISTENER)); + assertThat(taskExecutionListenerResults.size()).isEqualTo(6); + assertThat(taskExecutionListenerResults.get(0).getTaskName()) + .isEqualTo(BEFORE_LISTENER); + assertThat(taskExecutionListenerResults.get(1).getTaskName()) + .isEqualTo(BEFORE_LISTENER); + assertThat(taskExecutionListenerResults.get(2).getTaskName()) + .isEqualTo(FAIL_LISTENER); + assertThat(taskExecutionListenerResults.get(3).getTaskName()) + .isEqualTo(FAIL_LISTENER); + assertThat(taskExecutionListenerResults.get(4).getTaskName()) + .isEqualTo(AFTER_LISTENER); + assertThat(taskExecutionListenerResults.get(5).getTaskName()) + .isEqualTo(AFTER_LISTENER); + }); } private TaskExecution createSampleTaskExecution(String taskName) { @@ -133,6 +239,12 @@ public class TaskListenerExecutorObjectFactoryTests { assertThat(taskExecutionListenerResults.get(0).getTaskName()).isEqualTo(event); } + private void validateSingleEventWithMultipleInstances(String event) { + assertThat(taskExecutionListenerResults.size()).isEqualTo(2); + assertThat(taskExecutionListenerResults) + .allSatisfy(task -> assertThat(task.getTaskName()).isEqualTo(event)); + } + @Configuration public static class TaskExecutionListenerConfiguration { @@ -143,6 +255,20 @@ public class TaskListenerExecutorObjectFactoryTests { } + @Configuration + public static class TaskExecutionListenerMultipleInstanceConfiguration { + + @Bean + public TaskRunComponent taskRunComponent() { + return new TaskRunComponent(); + } + + @Bean + public TaskRunComponent otherTaskRunComponent() { + return new TaskRunComponent(); + } + } + public static class TaskRunComponent { @BeforeTask