Skip to content

Commit f33874e

Browse files
committed
Allow auto-configured applicationTaskExecutor to use virtual threads
With this commit, when virtual threads are enabled, the auto-configured applicationTaskExecutor changes from a ThreadPoolTaskExecutor to a SimpleAsyncTaskExecutor with virtual threads enabled. As before, any TaskDecorator bean is applied to the auto-configured executor and the spring.task.execution.thread-name-prefix property is applied. Other spring.task.execution.* properties are ignored as they are specific to a pool-based executor. Closes gh-35710
1 parent 783bfb6 commit f33874e

File tree

4 files changed

+157
-3
lines changed

4 files changed

+157
-3
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java

+3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.boot.task.TaskExecutorBuilder;
2929
import org.springframework.boot.task.TaskExecutorCustomizer;
3030
import org.springframework.context.annotation.Bean;
31+
import org.springframework.context.annotation.Import;
3132
import org.springframework.context.annotation.Lazy;
3233
import org.springframework.core.task.TaskDecorator;
3334
import org.springframework.core.task.TaskExecutor;
@@ -44,6 +45,8 @@
4445
@ConditionalOnClass(ThreadPoolTaskExecutor.class)
4546
@AutoConfiguration
4647
@EnableConfigurationProperties(TaskExecutionProperties.class)
48+
@Import({ TaskExecutorConfigurations.VirtualThreadTaskExecutorConfiguration.class,
49+
TaskExecutorConfigurations.ThreadPoolTaskExecutorConfiguration.class })
4750
public class TaskExecutionAutoConfiguration {
4851

4952
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.task;
18+
19+
import java.util.concurrent.Executor;
20+
21+
import org.springframework.beans.factory.ObjectProvider;
22+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
23+
import org.springframework.boot.autoconfigure.condition.ConditionalOnVirtualThreads;
24+
import org.springframework.boot.task.TaskExecutorBuilder;
25+
import org.springframework.context.annotation.Bean;
26+
import org.springframework.context.annotation.Configuration;
27+
import org.springframework.context.annotation.Lazy;
28+
import org.springframework.core.task.SimpleAsyncTaskExecutor;
29+
import org.springframework.core.task.TaskDecorator;
30+
import org.springframework.core.task.TaskExecutor;
31+
import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor;
32+
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
33+
34+
/**
35+
* {@link TaskExecutor} configurations to be imported by
36+
* {@link TaskExecutionAutoConfiguration} in a specific order.
37+
*
38+
* @author Andy Wilkinson
39+
*/
40+
class TaskExecutorConfigurations {
41+
42+
@ConditionalOnVirtualThreads
43+
@Configuration(proxyBeanMethods = false)
44+
@ConditionalOnMissingBean(Executor.class)
45+
static class VirtualThreadTaskExecutorConfiguration {
46+
47+
@Bean(name = { TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME,
48+
AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME })
49+
SimpleAsyncTaskExecutor applicationTaskExecutor(TaskExecutionProperties properties,
50+
ObjectProvider<TaskDecorator> taskDecorator) {
51+
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(properties.getThreadNamePrefix());
52+
executor.setVirtualThreads(true);
53+
executor.setTaskDecorator(taskDecorator.getIfUnique());
54+
return executor;
55+
}
56+
57+
}
58+
59+
@Configuration(proxyBeanMethods = false)
60+
@ConditionalOnMissingBean(Executor.class)
61+
static class ThreadPoolTaskExecutorConfiguration {
62+
63+
@Lazy
64+
@Bean(name = { TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME,
65+
AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME })
66+
ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) {
67+
return builder.build();
68+
}
69+
70+
}
71+
72+
}

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java

+77-1
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@
1717
package org.springframework.boot.autoconfigure.task;
1818

1919
import java.util.concurrent.CompletableFuture;
20+
import java.util.concurrent.CountDownLatch;
2021
import java.util.concurrent.Executor;
2122
import java.util.concurrent.Future;
23+
import java.util.concurrent.TimeUnit;
24+
import java.util.concurrent.atomic.AtomicReference;
2225
import java.util.function.Consumer;
2326

2427
import org.junit.jupiter.api.Test;
28+
import org.junit.jupiter.api.condition.DisabledForJreRange;
29+
import org.junit.jupiter.api.condition.JRE;
2530
import org.junit.jupiter.api.extension.ExtendWith;
2631

2732
import org.springframework.beans.factory.config.BeanDefinition;
@@ -34,6 +39,7 @@
3439
import org.springframework.boot.test.system.OutputCaptureExtension;
3540
import org.springframework.context.annotation.Bean;
3641
import org.springframework.context.annotation.Configuration;
42+
import org.springframework.core.task.SimpleAsyncTaskExecutor;
3743
import org.springframework.core.task.SyncTaskExecutor;
3844
import org.springframework.core.task.TaskDecorator;
3945
import org.springframework.core.task.TaskExecutor;
@@ -98,7 +104,7 @@ void taskExecutorBuilderShouldUseTaskDecorator() {
98104
}
99105

100106
@Test
101-
void taskExecutorAutoConfiguredIsLazy() {
107+
void whenThreadPoolTaskExecutorIsAutoConfiguredThenItIsLazy() {
102108
this.contextRunner.run((context) -> {
103109
assertThat(context).hasSingleBean(Executor.class).hasBean("applicationTaskExecutor");
104110
BeanDefinition beanDefinition = context.getSourceApplicationContext()
@@ -109,6 +115,51 @@ void taskExecutorAutoConfiguredIsLazy() {
109115
});
110116
}
111117

118+
@Test
119+
@DisabledForJreRange(max = JRE.JAVA_20)
120+
void whenVirtualThreadsAreEnabledThenSimpleAsyncTaskExecutorWithVirtualThreadsIsAutoConfigured() {
121+
this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> {
122+
assertThat(context).hasSingleBean(Executor.class).hasBean("applicationTaskExecutor");
123+
assertThat(context).getBean("applicationTaskExecutor").isInstanceOf(SimpleAsyncTaskExecutor.class);
124+
SimpleAsyncTaskExecutor taskExecutor = context.getBean("applicationTaskExecutor",
125+
SimpleAsyncTaskExecutor.class);
126+
assertThat(virtualThreadName(taskExecutor)).startsWith("task-");
127+
});
128+
}
129+
130+
@Test
131+
@DisabledForJreRange(max = JRE.JAVA_20)
132+
void whenTaskNamePrefixIsConfiguredThenSimpleAsyncTaskExecutorWithVirtualThreadsUsesIt() {
133+
this.contextRunner
134+
.withPropertyValues("spring.threads.virtual.enabled=true",
135+
"spring.task.execution.thread-name-prefix=custom-")
136+
.run((context) -> {
137+
SimpleAsyncTaskExecutor taskExecutor = context.getBean("applicationTaskExecutor",
138+
SimpleAsyncTaskExecutor.class);
139+
assertThat(virtualThreadName(taskExecutor)).startsWith("custom-");
140+
});
141+
}
142+
143+
@Test
144+
@DisabledForJreRange(max = JRE.JAVA_20)
145+
void whenVirtualThreadsAreAvailableButNotEnabledThenThreadPoolTaskExecutorIsAutoConfigured() {
146+
this.contextRunner.run((context) -> {
147+
assertThat(context).hasSingleBean(Executor.class).hasBean("applicationTaskExecutor");
148+
assertThat(context).getBean("applicationTaskExecutor").isInstanceOf(ThreadPoolTaskExecutor.class);
149+
});
150+
}
151+
152+
@Test
153+
@DisabledForJreRange(max = JRE.JAVA_20)
154+
void whenTaskDecoratorIsDefinedThenSimpleAsyncTaskExecutorWithVirtualThreadsUsesIt() {
155+
this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true")
156+
.withUserConfiguration(TaskDecoratorConfig.class)
157+
.run((context) -> {
158+
SimpleAsyncTaskExecutor executor = context.getBean(SimpleAsyncTaskExecutor.class);
159+
assertThat(executor).extracting("taskDecorator").isSameAs(context.getBean(TaskDecorator.class));
160+
});
161+
}
162+
112163
@Test
113164
void taskExecutorWhenHasCustomTaskExecutorShouldBackOff() {
114165
this.contextRunner.withUserConfiguration(CustomTaskExecutorConfig.class).run((context) -> {
@@ -117,6 +168,17 @@ void taskExecutorWhenHasCustomTaskExecutorShouldBackOff() {
117168
});
118169
}
119170

171+
@Test
172+
@DisabledForJreRange(max = JRE.JAVA_20)
173+
void whenVirtualThreadsAreEnabledAndCustomTaskExecutorIsDefinedThenSimpleAsyncTaskExecutorThatUsesVirtualThreadsBacksOff() {
174+
this.contextRunner.withUserConfiguration(CustomTaskExecutorConfig.class)
175+
.withPropertyValues("spring.threads.virtual.enabled=true")
176+
.run((context) -> {
177+
assertThat(context).hasSingleBean(Executor.class);
178+
assertThat(context.getBean(Executor.class)).isSameAs(context.getBean("customTaskExecutor"));
179+
});
180+
}
181+
120182
@Test
121183
void taskExecutorBuilderShouldApplyCustomizer() {
122184
this.contextRunner.withUserConfiguration(TaskExecutorCustomizerConfig.class).run((context) -> {
@@ -159,6 +221,20 @@ private ContextConsumer<AssertableApplicationContext> assertTaskExecutor(
159221
};
160222
}
161223

224+
private String virtualThreadName(SimpleAsyncTaskExecutor taskExecutor) throws InterruptedException {
225+
AtomicReference<Thread> threadReference = new AtomicReference<>();
226+
CountDownLatch latch = new CountDownLatch(1);
227+
taskExecutor.execute(() -> {
228+
Thread currentThread = Thread.currentThread();
229+
threadReference.set(currentThread);
230+
latch.countDown();
231+
});
232+
latch.await(30, TimeUnit.SECONDS);
233+
Thread thread = threadReference.get();
234+
assertThat(thread).extracting("virtual").as("%s is virtual", thread).isEqualTo(true);
235+
return thread.getName();
236+
}
237+
162238
@Configuration(proxyBeanMethods = false)
163239
static class CustomTaskExecutorBuilderConfig {
164240

spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
[[features.task-execution-and-scheduling]]
22
== Task Execution and Scheduling
3-
In the absence of an `Executor` bean in the context, Spring Boot auto-configures a `ThreadPoolTaskExecutor` with sensible defaults that can be automatically associated to asynchronous task execution (`@EnableAsync`) and Spring MVC asynchronous request processing.
3+
In the absence of an `Executor` bean in the context, Spring Boot auto-configures an `AsyncTaskExecutor`.
4+
When virtual threads are enabled (using Java 21+ and configprop:spring.threads.virtual.enabled[] set to `true`) this will be a `SimpleAsyncTaskExecutor` that uses virtual threads.
5+
Otherwise, it will be a `ThreadPoolTaskExecutor` with sensible defaults.
6+
In either case, the auto-configured executor will be automatically used for asynchronous task execution (`@EnableAsync`) and Spring MVC asynchronous request processing.
47

58
[TIP]
69
====
@@ -10,7 +13,7 @@ Depending on your target arrangement, you could change your `Executor` into a `T
1013
The auto-configured `TaskExecutorBuilder` allows you to easily create instances that reproduce what the auto-configuration does by default.
1114
====
1215

13-
The thread pool uses 8 core threads that can grow and shrink according to the load.
16+
When a `ThreadPoolTaskExecutor` is auto-configured, the thread pool uses 8 core threads that can grow and shrink according to the load.
1417
Those default settings can be fine-tuned using the `spring.task.execution` namespace, as shown in the following example:
1518

1619
[source,yaml,indent=0,subs="verbatim",configprops,configblocks]

0 commit comments

Comments
 (0)