Add support for multiple TaskDecorator beans

Previously, only a single TaskDecorator bean, if unique, was applied
to the auto-configured TaskExecutor or Scheduler. With this change,
if multiple TaskDecorator beans are defined, they will be combined
into a `CompositeTaskDecorator` and applied accordingly.

Signed-off-by: Dmytro Nosan <dimanosan@gmail.com>

See gh-45302
This commit is contained in:
Dmytro Nosan
2025-07-02 11:55:57 +01:00
committed by Andy Wilkinson
parent ac2656d312
commit fff816d056
4 changed files with 113 additions and 34 deletions
@@ -16,6 +16,7 @@
package org.springframework.boot.autoconfigure.task;
import java.util.List;
import java.util.concurrent.Executor;
import org.springframework.beans.factory.BeanFactory;
@@ -39,6 +40,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.TaskDecorator;
import org.springframework.core.task.TaskExecutor;
import org.springframework.core.task.support.CompositeTaskDecorator;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@@ -52,6 +54,14 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
*/
class TaskExecutorConfigurations {
private static TaskDecorator getTaskDecorator(ObjectProvider<TaskDecorator> taskDecorator) {
List<TaskDecorator> taskDecorators = taskDecorator.orderedStream().toList();
if (taskDecorators.size() == 1) {
return taskDecorators.get(0);
}
return (!taskDecorators.isEmpty()) ? new CompositeTaskDecorator(taskDecorators) : null;
}
@Configuration(proxyBeanMethods = false)
@Conditional(OnExecutorCondition.class)
@Import(AsyncConfigurerConfiguration.class)
@@ -93,7 +103,7 @@ class TaskExecutorConfigurations {
builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod());
builder = builder.threadNamePrefix(properties.getThreadNamePrefix());
builder = builder.customizers(threadPoolTaskExecutorCustomizers.orderedStream()::iterator);
builder = builder.taskDecorator(taskDecorator.getIfUnique());
builder = builder.taskDecorator(getTaskDecorator(taskDecorator));
return builder;
}
@@ -134,7 +144,7 @@ class TaskExecutorConfigurations {
SimpleAsyncTaskExecutorBuilder builder = new SimpleAsyncTaskExecutorBuilder();
builder = builder.threadNamePrefix(this.properties.getThreadNamePrefix());
builder = builder.customizers(this.taskExecutorCustomizers.orderedStream()::iterator);
builder = builder.taskDecorator(this.taskDecorator.getIfUnique());
builder = builder.taskDecorator(getTaskDecorator(this.taskDecorator));
TaskExecutionProperties.Simple simple = this.properties.getSimple();
builder = builder.rejectTasksWhenLimitReached(simple.isRejectTasksWhenLimitReached());
builder = builder.concurrencyLimit(simple.getConcurrencyLimit());
@@ -16,6 +16,7 @@
package org.springframework.boot.autoconfigure.task;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import org.springframework.beans.factory.ObjectProvider;
@@ -30,6 +31,7 @@ import org.springframework.boot.task.ThreadPoolTaskSchedulerCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskDecorator;
import org.springframework.core.task.support.CompositeTaskDecorator;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@@ -43,6 +45,14 @@ import org.springframework.scheduling.config.TaskManagementConfigUtils;
*/
class TaskSchedulingConfigurations {
private static TaskDecorator getTaskDecorator(ObjectProvider<TaskDecorator> taskDecorator) {
List<TaskDecorator> taskDecorators = taskDecorator.orderedStream().toList();
if (taskDecorators.size() == 1) {
return taskDecorators.get(0);
}
return (!taskDecorators.isEmpty()) ? new CompositeTaskDecorator(taskDecorators) : null;
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
@ConditionalOnMissingBean({ TaskScheduler.class, ScheduledExecutorService.class })
@@ -76,7 +86,7 @@ class TaskSchedulingConfigurations {
builder = builder.awaitTermination(shutdown.isAwaitTermination());
builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod());
builder = builder.threadNamePrefix(properties.getThreadNamePrefix());
builder = builder.taskDecorator(taskDecorator.getIfUnique());
builder = builder.taskDecorator(getTaskDecorator(taskDecorator));
builder = builder.customizers(threadPoolTaskSchedulerCustomizers);
return builder;
}
@@ -117,7 +127,7 @@ class TaskSchedulingConfigurations {
private SimpleAsyncTaskSchedulerBuilder builder() {
SimpleAsyncTaskSchedulerBuilder builder = new SimpleAsyncTaskSchedulerBuilder();
builder = builder.threadNamePrefix(this.properties.getThreadNamePrefix());
builder = builder.taskDecorator(this.taskDecorator.getIfUnique());
builder = builder.taskDecorator(getTaskDecorator(this.taskDecorator));
builder = builder.customizers(this.taskSchedulerCustomizers.orderedStream()::iterator);
TaskSchedulingProperties.Simple simple = this.properties.getSimple();
builder = builder.concurrencyLimit(simple.getConcurrencyLimit());
@@ -24,6 +24,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
@@ -46,6 +47,7 @@ import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.core.task.TaskDecorator;
import org.springframework.core.task.TaskExecutor;
import org.springframework.core.task.support.CompositeTaskDecorator;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
@@ -53,7 +55,6 @@ import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link TaskExecutionAutoConfiguration}.
@@ -127,13 +128,29 @@ class TaskExecutionAutoConfigurationTests {
@Test
void threadPoolTaskExecutorBuilderShouldUseTaskDecorator() {
this.contextRunner.withUserConfiguration(TaskDecoratorConfig.class).run((context) -> {
this.contextRunner.withBean(TaskDecorator.class, this::createTaskDecorator).run((context) -> {
assertThat(context).hasSingleBean(ThreadPoolTaskExecutorBuilder.class);
ThreadPoolTaskExecutor executor = context.getBean(ThreadPoolTaskExecutorBuilder.class).build();
assertThat(executor).extracting("taskDecorator").isSameAs(context.getBean(TaskDecorator.class));
});
}
@Test
void threadPoolTaskExecutorBuilderShouldUseCompositeTaskDecorator() {
this.contextRunner.withBean("taskDecorator1", TaskDecorator.class, this::createTaskDecorator)
.withBean("taskDecorator2", TaskDecorator.class, this::createTaskDecorator)
.run((context) -> {
assertThat(context).hasSingleBean(ThreadPoolTaskExecutorBuilder.class);
ThreadPoolTaskExecutor executor = context.getBean(ThreadPoolTaskExecutorBuilder.class).build();
assertThat(executor).extracting("taskDecorator")
.isInstanceOf(CompositeTaskDecorator.class)
.extracting("taskDecorators")
.asInstanceOf(InstanceOfAssertFactories.list(TaskDecorator.class))
.containsExactly(context.getBean("taskDecorator1", TaskDecorator.class),
context.getBean("taskDecorator2", TaskDecorator.class));
});
}
@Test
void whenThreadPoolTaskExecutorIsAutoConfiguredThenItIsLazy() {
this.contextRunner.run((context) -> {
@@ -184,13 +201,30 @@ class TaskExecutionAutoConfigurationTests {
@EnabledForJreRange(min = JRE.JAVA_21)
void whenTaskDecoratorIsDefinedThenSimpleAsyncTaskExecutorWithVirtualThreadsUsesIt() {
this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true")
.withUserConfiguration(TaskDecoratorConfig.class)
.withBean(TaskDecorator.class, this::createTaskDecorator)
.run((context) -> {
SimpleAsyncTaskExecutor executor = context.getBean(SimpleAsyncTaskExecutor.class);
assertThat(executor).extracting("taskDecorator").isSameAs(context.getBean(TaskDecorator.class));
});
}
@Test
@EnabledForJreRange(min = JRE.JAVA_21)
void whenTaskDecoratorsAreDefinedThenSimpleAsyncTaskExecutorWithVirtualThreadsUsesThem() {
this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true")
.withBean("taskDecorator1", TaskDecorator.class, this::createTaskDecorator)
.withBean("taskDecorator2", TaskDecorator.class, this::createTaskDecorator)
.run((context) -> {
SimpleAsyncTaskExecutor executor = context.getBean(SimpleAsyncTaskExecutor.class);
assertThat(executor).extracting("taskDecorator")
.isInstanceOf(CompositeTaskDecorator.class)
.extracting("taskDecorators")
.asInstanceOf(InstanceOfAssertFactories.list(TaskDecorator.class))
.containsExactly(context.getBean("taskDecorator1", TaskDecorator.class),
context.getBean("taskDecorator2", TaskDecorator.class));
});
}
@Test
void simpleAsyncTaskExecutorBuilderUsesPlatformThreadsByDefault() {
this.contextRunner.run((context) -> {
@@ -451,6 +485,10 @@ class TaskExecutionAutoConfigurationTests {
});
}
private TaskDecorator createTaskDecorator() {
return (runnable) -> runnable;
}
private Executor createCustomAsyncExecutor(String threadNamePrefix) {
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
executor.setThreadNamePrefix(threadNamePrefix);
@@ -501,16 +539,6 @@ class TaskExecutionAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class TaskDecoratorConfig {
@Bean
TaskDecorator mockTaskDecorator() {
return mock(TaskDecorator.class);
}
}
@Configuration(proxyBeanMethods = false)
@EnableAsync
static class AsyncConfiguration {
@@ -43,15 +43,16 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskDecorator;
import org.springframework.core.task.TaskExecutor;
import org.springframework.core.task.support.CompositeTaskDecorator;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link TaskSchedulingAutoConfiguration}.
@@ -143,25 +144,61 @@ class TaskSchedulingAutoConfigurationTests {
@Test
void simpleAsyncTaskSchedulerBuilderShouldApplyTaskDecorator() {
this.contextRunner.withUserConfiguration(SchedulingConfiguration.class, TaskDecoratorConfig.class)
this.contextRunner.withUserConfiguration(SchedulingConfiguration.class)
.withBean(TaskDecorator.class, this::createTaskDecorator)
.run((context) -> {
assertThat(context).hasSingleBean(SimpleAsyncTaskSchedulerBuilder.class);
assertThat(context).hasSingleBean(TaskDecorator.class);
TaskDecorator taskDecorator = context.getBean(TaskDecorator.class);
SimpleAsyncTaskSchedulerBuilder builder = context.getBean(SimpleAsyncTaskSchedulerBuilder.class);
assertThat(builder).extracting("taskDecorator").isSameAs(taskDecorator);
SimpleAsyncTaskScheduler scheduler = context.getBean(SimpleAsyncTaskSchedulerBuilder.class).build();
assertThat(scheduler).extracting("taskDecorator").isSameAs(taskDecorator);
});
}
@Test
void simpleAsyncTaskSchedulerBuilderShouldApplyCompositeTaskDecorator() {
this.contextRunner.withUserConfiguration(SchedulingConfiguration.class)
.withBean("taskDecorator1", TaskDecorator.class, this::createTaskDecorator)
.withBean("taskDecorator2", TaskDecorator.class, this::createTaskDecorator)
.run((context) -> {
assertThat(context).hasSingleBean(SimpleAsyncTaskSchedulerBuilder.class);
SimpleAsyncTaskScheduler scheduler = context.getBean(SimpleAsyncTaskSchedulerBuilder.class).build();
assertThat(scheduler).extracting("taskDecorator")
.isInstanceOf(CompositeTaskDecorator.class)
.extracting("taskDecorators")
.asInstanceOf(InstanceOfAssertFactories.list(TaskDecorator.class))
.containsExactly(context.getBean("taskDecorator1", TaskDecorator.class),
context.getBean("taskDecorator2", TaskDecorator.class));
});
}
@Test
void threadPoolTaskSchedulerBuilderShouldApplyTaskDecorator() {
this.contextRunner.withUserConfiguration(SchedulingConfiguration.class, TaskDecoratorConfig.class)
this.contextRunner.withUserConfiguration(SchedulingConfiguration.class)
.withBean(TaskDecorator.class, this::createTaskDecorator)
.run((context) -> {
assertThat(context).hasSingleBean(ThreadPoolTaskSchedulerBuilder.class);
assertThat(context).hasSingleBean(TaskDecorator.class);
TaskDecorator taskDecorator = context.getBean(TaskDecorator.class);
ThreadPoolTaskSchedulerBuilder builder = context.getBean(ThreadPoolTaskSchedulerBuilder.class);
assertThat(builder).extracting("taskDecorator").isSameAs(taskDecorator);
ThreadPoolTaskScheduler scheduler = context.getBean(ThreadPoolTaskSchedulerBuilder.class).build();
assertThat(scheduler).extracting("taskDecorator").isSameAs(taskDecorator);
});
}
@Test
void threadPoolTaskSchedulerBuilderShouldApplyCompositeTaskDecorator() {
this.contextRunner.withUserConfiguration(SchedulingConfiguration.class)
.withBean("taskDecorator1", TaskDecorator.class, this::createTaskDecorator)
.withBean("taskDecorator2", TaskDecorator.class, this::createTaskDecorator)
.run((context) -> {
assertThat(context).hasSingleBean(ThreadPoolTaskSchedulerBuilder.class);
ThreadPoolTaskScheduler scheduler = context.getBean(ThreadPoolTaskSchedulerBuilder.class).build();
assertThat(scheduler).extracting("taskDecorator")
.isInstanceOf(CompositeTaskDecorator.class)
.extracting("taskDecorators")
.asInstanceOf(InstanceOfAssertFactories.list(TaskDecorator.class))
.containsExactly(context.getBean("taskDecorator1", TaskDecorator.class),
context.getBean("taskDecorator2", TaskDecorator.class));
});
}
@@ -234,6 +271,10 @@ class TaskSchedulingAutoConfigurationTests {
});
}
private TaskDecorator createTaskDecorator() {
return (runnable) -> runnable;
}
@Configuration(proxyBeanMethods = false)
@EnableScheduling
static class SchedulingConfiguration {
@@ -331,14 +372,4 @@ class TaskSchedulingAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class TaskDecoratorConfig {
@Bean
TaskDecorator mockTaskDecorator() {
return mock(TaskDecorator.class);
}
}
}