Allow configuring a TaskExecutor even if an Executor is present

This commit updates `TaskExecutionAutoConfiguration` to permit the
auto-configuration of a `TaskExecutor` even if a user-defined `Executor`
bean is present.

Such `Executor` may have been created for totally unrelated reason, and
it may or may not be an `AsyncTaskExecutor`. The default behavior has
not changed, but this commit provides a new property,
`spring.task.execution.mode` that can be set to `force` to
auto-configure the `TaskExecutor` anyway.

Because this mode made it so that two `Executor` will be present in the
context, this commit also automatically configures an `AsyncConfigurer`
if none is present already to make sure task processing uses the
auto-configured TaskExecutor.

Closes gh-44659
This commit is contained in:
Stéphane Nicoll 2025-03-26 10:04:24 +01:00
parent ac550945fe
commit 13bd61445b
4 changed files with 216 additions and 34 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 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.
@ -37,6 +37,11 @@ public class TaskExecutionProperties {
private final Shutdown shutdown = new Shutdown();
/**
* Determine when the task executor is to be created.
*/
private Mode mode = Mode.AUTO;
/**
* Prefix to use for the names of newly created threads.
*/
@ -54,6 +59,14 @@ public class TaskExecutionProperties {
return this.shutdown;
}
public Mode getMode() {
return this.mode;
}
public void setMode(Mode mode) {
this.mode = mode;
}
public String getThreadNamePrefix() {
return this.threadNamePrefix;
}
@ -209,4 +222,23 @@ public class TaskExecutionProperties {
}
/**
* Determine when the task executor is to be created.
*
* @since 3.5.0
*/
public enum Mode {
/**
* Create the task executor if no user-defined executor is present.
*/
AUTO,
/**
* Create the task executor even if a user-defined executor is present.
*/
FORCE
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 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.
@ -18,8 +18,11 @@ package org.springframework.boot.autoconfigure.task;
import java.util.concurrent.Executor;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading;
import org.springframework.boot.autoconfigure.thread.Threading;
import org.springframework.boot.task.SimpleAsyncTaskExecutorBuilder;
@ -27,12 +30,15 @@ import org.springframework.boot.task.SimpleAsyncTaskExecutorCustomizer;
import org.springframework.boot.task.ThreadPoolTaskExecutorBuilder;
import org.springframework.boot.task.ThreadPoolTaskExecutorCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
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.scheduling.annotation.AsyncAnnotationBeanPostProcessor;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
@ -46,19 +52,18 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
class TaskExecutorConfigurations {
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(Executor.class)
@Conditional(OnExecutorCondition.class)
@Import(AsyncConfigurerConfiguration.class)
static class TaskExecutorConfiguration {
@Bean(name = { TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME,
AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME })
@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
@ConditionalOnThreading(Threading.VIRTUAL)
SimpleAsyncTaskExecutor applicationTaskExecutorVirtualThreads(SimpleAsyncTaskExecutorBuilder builder) {
return builder.build();
}
@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
@Lazy
@Bean(name = { TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME,
AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME })
@ConditionalOnThreading(Threading.PLATFORM)
ThreadPoolTaskExecutor applicationTaskExecutor(ThreadPoolTaskExecutorBuilder threadPoolTaskExecutorBuilder) {
return threadPoolTaskExecutorBuilder.build();
@ -140,4 +145,41 @@ class TaskExecutorConfigurations {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(name = AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME,
value = AsyncConfigurer.class)
static class AsyncConfigurerConfiguration {
@Bean
@ConditionalOnMissingBean
AsyncConfigurer applicationTaskExecutorAsyncConfigurer(BeanFactory beanFactory) {
return new AsyncConfigurer() {
@Override
public Executor getAsyncExecutor() {
return beanFactory.getBean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME,
Executor.class);
}
};
}
}
static class OnExecutorCondition extends AnyNestedCondition {
OnExecutorCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnMissingBean(Executor.class)
private static final class ExecutorBeanCondition {
}
@ConditionalOnProperty(value = "spring.task.execution.mode", havingValue = "force")
private static final class ModelCondition {
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 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.
@ -30,6 +30,7 @@ import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionOverrideException;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.task.SimpleAsyncTaskExecutorBuilder;
import org.springframework.boot.task.ThreadPoolTaskExecutorBuilder;
@ -44,6 +45,7 @@ import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.core.task.TaskDecorator;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@ -204,16 +206,43 @@ class TaskExecutionAutoConfigurationTests {
@Test
void taskExecutorWhenHasCustomTaskExecutorShouldBackOff() {
this.contextRunner.withUserConfiguration(CustomTaskExecutorConfig.class).run((context) -> {
this.contextRunner.withBean("customTaskExecutor", Executor.class, SyncTaskExecutor::new).run((context) -> {
assertThat(context).hasSingleBean(Executor.class);
assertThat(context.getBean(Executor.class)).isSameAs(context.getBean("customTaskExecutor"));
});
}
@Test
void taskExecutorWhenModeIsAutoAndHasCustomTaskExecutorShouldBackOff() {
this.contextRunner.withBean("customTaskExecutor", Executor.class, SyncTaskExecutor::new)
.withPropertyValues("spring.task.execution.mode=auto")
.run((context) -> {
assertThat(context).hasSingleBean(Executor.class);
assertThat(context.getBean(Executor.class)).isSameAs(context.getBean("customTaskExecutor"));
});
}
@Test
void taskExecutorWhenModeIsForceAndHasCustomTaskExecutorShouldCreateApplicationTaskExecutor() {
this.contextRunner.withBean("customTaskExecutor", Executor.class, SyncTaskExecutor::new)
.withPropertyValues("spring.task.execution.mode=force")
.run((context) -> assertThat(context.getBeansOfType(Executor.class)).hasSize(2)
.containsKeys("customTaskExecutor", "applicationTaskExecutor"));
}
@Test
void taskExecutorWhenModeIsForceAndHasCustomTaskExecutorWithReservedNameShouldThrowException() {
this.contextRunner.withBean("applicationTaskExecutor", Executor.class, SyncTaskExecutor::new)
.withPropertyValues("spring.task.execution.mode=force")
.run((context) -> assertThat(context).hasFailed()
.getFailure()
.isInstanceOf(BeanDefinitionOverrideException.class));
}
@Test
@EnabledForJreRange(min = JRE.JAVA_21)
void whenVirtualThreadsAreEnabledAndCustomTaskExecutorIsDefinedThenSimpleAsyncTaskExecutorThatUsesVirtualThreadsBacksOff() {
this.contextRunner.withUserConfiguration(CustomTaskExecutorConfig.class)
this.contextRunner.withBean("customTaskExecutor", Executor.class, SyncTaskExecutor::new)
.withPropertyValues("spring.threads.virtual.enabled=true")
.run((context) -> {
assertThat(context).hasSingleBean(Executor.class);
@ -223,25 +252,101 @@ class TaskExecutionAutoConfigurationTests {
@Test
void enableAsyncUsesAutoConfiguredOneByDefault() {
this.contextRunner.withPropertyValues("spring.task.execution.thread-name-prefix=task-test-")
this.contextRunner.withPropertyValues("spring.task.execution.thread-name-prefix=auto-task-")
.withUserConfiguration(AsyncConfiguration.class, TestBean.class)
.run((context) -> {
assertThat(context).hasSingleBean(AsyncConfigurer.class);
assertThat(context).hasSingleBean(TaskExecutor.class);
TestBean bean = context.getBean(TestBean.class);
String text = bean.echo("something").get();
assertThat(text).contains("task-test-").contains("something");
assertThat(text).contains("auto-task-").contains("something");
});
}
@Test
void enableAsyncUsesCustomExecutorIfPresent() {
this.contextRunner.withPropertyValues("spring.task.execution.thread-name-prefix=auto-task-")
.withBean("customTaskExecutor", Executor.class, () -> createCustomAsyncExecutor("custom-task-"))
.withUserConfiguration(AsyncConfiguration.class, TestBean.class)
.run((context) -> {
assertThat(context).doesNotHaveBean(AsyncConfigurer.class);
assertThat(context).hasSingleBean(Executor.class);
TestBean bean = context.getBean(TestBean.class);
String text = bean.echo("something").get();
assertThat(text).contains("custom-task-").contains("something");
});
}
@Test
void enableAsyncUsesAutoConfiguredExecutorWhenModeIsForceAndHasCustomTaskExecutor() {
this.contextRunner
.withPropertyValues("spring.task.execution.thread-name-prefix=auto-task-",
"spring.task.execution.mode=force")
.withBean("customTaskExecutor", Executor.class, () -> createCustomAsyncExecutor("custom-task-"))
.withUserConfiguration(AsyncConfiguration.class, TestBean.class)
.run((context) -> {
assertThat(context).hasSingleBean(AsyncConfigurer.class);
assertThat(context.getBeansOfType(Executor.class)).hasSize(2);
TestBean bean = context.getBean(TestBean.class);
String text = bean.echo("something").get();
assertThat(text).contains("auto-task-").contains("something");
});
}
@Test
void enableAsyncUsesCustomExecutorWhenModeIsForceAndHasCustomTaskExecutorWithReservedName() {
this.contextRunner
.withPropertyValues("spring.task.execution.thread-name-prefix=auto-task-",
"spring.task.execution.mode=force")
.withBean("taskExecutor", Executor.class, () -> createCustomAsyncExecutor("custom-task-"))
.withUserConfiguration(AsyncConfiguration.class, TestBean.class)
.run((context) -> {
assertThat(context).doesNotHaveBean(AsyncConfigurer.class);
assertThat(context.getBeansOfType(Executor.class)).hasSize(2);
TestBean bean = context.getBean(TestBean.class);
String text = bean.echo("something").get();
assertThat(text).contains("custom-task-").contains("something");
});
}
@Test
void enableAsyncUsesAsyncConfigurerWhenModeIsForce() {
this.contextRunner
.withPropertyValues("spring.task.execution.thread-name-prefix=auto-task-",
"spring.task.execution.mode=force")
.withBean("taskExecutor", Executor.class, () -> createCustomAsyncExecutor("custom-task-"))
.withBean("customAsyncConfigurer", AsyncConfigurer.class, () -> new AsyncConfigurer() {
@Override
public Executor getAsyncExecutor() {
return createCustomAsyncExecutor("async-task-");
}
})
.withUserConfiguration(AsyncConfiguration.class, TestBean.class)
.run((context) -> {
assertThat(context).hasSingleBean(AsyncConfigurer.class);
assertThat(context.getBeansOfType(Executor.class)).hasSize(2)
.containsOnlyKeys("taskExecutor", "applicationTaskExecutor");
TestBean bean = context.getBean(TestBean.class);
String text = bean.echo("something").get();
assertThat(text).contains("async-task-").contains("something");
});
}
private Executor createCustomAsyncExecutor(String threadNamePrefix) {
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
executor.setThreadNamePrefix(threadNamePrefix);
return executor;
}
@Test
void enableAsyncUsesAutoConfiguredOneByDefaultEvenThoughSchedulingIsConfigured() {
this.contextRunner.withPropertyValues("spring.task.execution.thread-name-prefix=task-test-")
this.contextRunner.withPropertyValues("spring.task.execution.thread-name-prefix=auto-task-")
.withConfiguration(AutoConfigurations.of(TaskSchedulingAutoConfiguration.class))
.withUserConfiguration(AsyncConfiguration.class, SchedulingConfiguration.class, TestBean.class)
.run((context) -> {
TestBean bean = context.getBean(TestBean.class);
String text = bean.echo("something").get();
assertThat(text).contains("task-test-").contains("something");
assertThat(text).contains("auto-task-").contains("something");
});
}
@ -299,16 +404,6 @@ class TaskExecutionAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class CustomTaskExecutorConfig {
@Bean
Executor customTaskExecutor() {
return new SyncTaskExecutor();
}
}
@Configuration(proxyBeanMethods = false)
@EnableAsync
static class AsyncConfiguration {

View File

@ -4,26 +4,39 @@
In the absence of an javadoc:java.util.concurrent.Executor[] bean in the context, Spring Boot auto-configures an javadoc:org.springframework.core.task.AsyncTaskExecutor[].
When virtual threads are enabled (using Java 21+ and configprop:spring.threads.virtual.enabled[] set to `true`) this will be a javadoc:org.springframework.core.task.SimpleAsyncTaskExecutor[] that uses virtual threads.
Otherwise, it will be a javadoc:org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor[] with sensible defaults.
In either case, the auto-configured executor will be automatically used for:
- asynchronous task execution (`@EnableAsync`)
- Spring for GraphQL's asynchronous handling of javadoc:java.util.concurrent.Callable[] return values from controller methods
- Spring MVC's asynchronous request processing
- Spring WebFlux's blocking execution support
If a custom `Executor` bean is present, you can request Spring Boot to auto-configure an `AsyncTaskExecutor` anyway, as follows:
[configprops,yaml]
----
spring:
task:
execution:
mode: force
----
The auto-configured executor will be automatically used for:
- Asynchronous task execution (`@EnableAsync`), unless an javadoc:org.springframework.scheduling.annotation.AsyncConfigurer[] bean is present.
- Spring for GraphQL's asynchronous handling of javadoc:java.util.concurrent.Callable[] return values from controller methods.
- Spring MVC's asynchronous request processing.
- Spring WebFlux's blocking execution support.
[TIP]
====
If you have defined a custom javadoc:java.util.concurrent.Executor[] in the context, both regular task execution (that is javadoc:org.springframework.scheduling.annotation.EnableAsync[format=annotation]) and Spring for GraphQL will use it.
However, the Spring MVC and Spring WebFlux support will only use it if it is an javadoc:org.springframework.core.task.AsyncTaskExecutor[] implementation (named `applicationTaskExecutor`).
Depending on your target arrangement, you could change your javadoc:java.util.concurrent.Executor[] into an javadoc:org.springframework.core.task.AsyncTaskExecutor[] or define both an javadoc:org.springframework.core.task.AsyncTaskExecutor[] and an javadoc:org.springframework.scheduling.annotation.AsyncConfigurer[] wrapping your custom javadoc:java.util.concurrent.Executor[].
However, the Spring MVC and Spring WebFlux support will only use it if it is an javadoc:org.springframework.core.task.AsyncTaskExecutor[] implementation named `applicationTaskExecutor`.
The auto-configured javadoc:org.springframework.boot.task.ThreadPoolTaskExecutorBuilder[] allows you to easily create instances that reproduce what the auto-configuration does by default.
Depending on your target arrangement, you could set configprop:spring.task.execution.mode[] to `force` to auto-configure an `applicationTaskExecutor`, change your javadoc:java.util.concurrent.Executor[] into an javadoc:org.springframework.core.task.AsyncTaskExecutor[] or define both an javadoc:org.springframework.core.task.AsyncTaskExecutor[] and an javadoc:org.springframework.scheduling.annotation.AsyncConfigurer[] wrapping your custom javadoc:java.util.concurrent.Executor[].
Another option is to define those beans explicitly.
The auto-configured javadoc:org.springframework.boot.task.ThreadPoolTaskExecutorBuilder[] or javadoc:org.springframework.boot.task.SimpleAsyncTaskExecutorBuilder[] allow you to easily create instances that reproduce what the auto-configuration does by default.
====
[NOTE]
====
If multiple javadoc:java.util.concurrent.Executor[] beans are defined, regular task execution fallbacks to a bean named `taskExecutor`.
GraphQL, Spring MVC and Spring WebFlux support fallback to a bean named `applicationTaskExecutor`.
If multiple javadoc:java.util.concurrent.Executor[] beans are defined with configprop:spring.task.execution.mode[] to `force`, all the supported integrations look for a bean named `applicationTaskExecutor`.
If the auto-configured `AsyncTaskExecutor` is not defined, only regular task execution fallbacks to a bean named `taskExecutor` to match Spring Framework's behavior.
====