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:
parent
ac550945fe
commit
13bd61445b
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
====
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue