diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java index 1ebb19871931..0ee647f712c0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java @@ -28,6 +28,7 @@ import org.springframework.boot.task.TaskExecutorBuilder; import org.springframework.boot.task.TaskExecutorCustomizer; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Lazy; import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.TaskExecutor; @@ -44,6 +45,8 @@ @ConditionalOnClass(ThreadPoolTaskExecutor.class) @AutoConfiguration @EnableConfigurationProperties(TaskExecutionProperties.class) +@Import({ TaskExecutorConfigurations.VirtualThreadTaskExecutorConfiguration.class, + TaskExecutorConfigurations.ThreadPoolTaskExecutorConfiguration.class }) public class TaskExecutionAutoConfiguration { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java new file mode 100644 index 000000000000..f80bbaf493b5 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2023 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.task; + +import java.util.concurrent.Executor; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnVirtualThreads; +import org.springframework.boot.task.TaskExecutorBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.concurrent.ThreadPoolTaskExecutor; + +/** + * {@link TaskExecutor} configurations to be imported by + * {@link TaskExecutionAutoConfiguration} in a specific order. + * + * @author Andy Wilkinson + */ +class TaskExecutorConfigurations { + + @ConditionalOnVirtualThreads + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(Executor.class) + static class VirtualThreadTaskExecutorConfiguration { + + @Bean(name = { TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME, + AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME }) + SimpleAsyncTaskExecutor applicationTaskExecutor(TaskExecutionProperties properties, + ObjectProvider taskDecorator) { + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(properties.getThreadNamePrefix()); + executor.setVirtualThreads(true); + executor.setTaskDecorator(taskDecorator.getIfUnique()); + return executor; + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(Executor.class) + static class ThreadPoolTaskExecutorConfiguration { + + @Lazy + @Bean(name = { TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME, + AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME }) + ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) { + return builder.build(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java index c1d0507903ac..8a1e09e922f0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java @@ -17,11 +17,16 @@ package org.springframework.boot.autoconfigure.task; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.config.BeanDefinition; @@ -34,6 +39,7 @@ import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.core.task.SyncTaskExecutor; import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.TaskExecutor; @@ -98,7 +104,7 @@ void taskExecutorBuilderShouldUseTaskDecorator() { } @Test - void taskExecutorAutoConfiguredIsLazy() { + void whenThreadPoolTaskExecutorIsAutoConfiguredThenItIsLazy() { this.contextRunner.run((context) -> { assertThat(context).hasSingleBean(Executor.class).hasBean("applicationTaskExecutor"); BeanDefinition beanDefinition = context.getSourceApplicationContext() @@ -109,6 +115,51 @@ void taskExecutorAutoConfiguredIsLazy() { }); } + @Test + @DisabledForJreRange(max = JRE.JAVA_20) + void whenVirtualThreadsAreEnabledThenSimpleAsyncTaskExecutorWithVirtualThreadsIsAutoConfigured() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { + assertThat(context).hasSingleBean(Executor.class).hasBean("applicationTaskExecutor"); + assertThat(context).getBean("applicationTaskExecutor").isInstanceOf(SimpleAsyncTaskExecutor.class); + SimpleAsyncTaskExecutor taskExecutor = context.getBean("applicationTaskExecutor", + SimpleAsyncTaskExecutor.class); + assertThat(virtualThreadName(taskExecutor)).startsWith("task-"); + }); + } + + @Test + @DisabledForJreRange(max = JRE.JAVA_20) + void whenTaskNamePrefixIsConfiguredThenSimpleAsyncTaskExecutorWithVirtualThreadsUsesIt() { + this.contextRunner + .withPropertyValues("spring.threads.virtual.enabled=true", + "spring.task.execution.thread-name-prefix=custom-") + .run((context) -> { + SimpleAsyncTaskExecutor taskExecutor = context.getBean("applicationTaskExecutor", + SimpleAsyncTaskExecutor.class); + assertThat(virtualThreadName(taskExecutor)).startsWith("custom-"); + }); + } + + @Test + @DisabledForJreRange(max = JRE.JAVA_20) + void whenVirtualThreadsAreAvailableButNotEnabledThenThreadPoolTaskExecutorIsAutoConfigured() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(Executor.class).hasBean("applicationTaskExecutor"); + assertThat(context).getBean("applicationTaskExecutor").isInstanceOf(ThreadPoolTaskExecutor.class); + }); + } + + @Test + @DisabledForJreRange(max = JRE.JAVA_20) + void whenTaskDecoratorIsDefinedThenSimpleAsyncTaskExecutorWithVirtualThreadsUsesIt() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true") + .withUserConfiguration(TaskDecoratorConfig.class) + .run((context) -> { + SimpleAsyncTaskExecutor executor = context.getBean(SimpleAsyncTaskExecutor.class); + assertThat(executor).extracting("taskDecorator").isSameAs(context.getBean(TaskDecorator.class)); + }); + } + @Test void taskExecutorWhenHasCustomTaskExecutorShouldBackOff() { this.contextRunner.withUserConfiguration(CustomTaskExecutorConfig.class).run((context) -> { @@ -117,6 +168,17 @@ void taskExecutorWhenHasCustomTaskExecutorShouldBackOff() { }); } + @Test + @DisabledForJreRange(max = JRE.JAVA_20) + void whenVirtualThreadsAreEnabledAndCustomTaskExecutorIsDefinedThenSimpleAsyncTaskExecutorThatUsesVirtualThreadsBacksOff() { + this.contextRunner.withUserConfiguration(CustomTaskExecutorConfig.class) + .withPropertyValues("spring.threads.virtual.enabled=true") + .run((context) -> { + assertThat(context).hasSingleBean(Executor.class); + assertThat(context.getBean(Executor.class)).isSameAs(context.getBean("customTaskExecutor")); + }); + } + @Test void taskExecutorBuilderShouldApplyCustomizer() { this.contextRunner.withUserConfiguration(TaskExecutorCustomizerConfig.class).run((context) -> { @@ -159,6 +221,20 @@ private ContextConsumer assertTaskExecutor( }; } + private String virtualThreadName(SimpleAsyncTaskExecutor taskExecutor) throws InterruptedException { + AtomicReference threadReference = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + taskExecutor.execute(() -> { + Thread currentThread = Thread.currentThread(); + threadReference.set(currentThread); + latch.countDown(); + }); + latch.await(30, TimeUnit.SECONDS); + Thread thread = threadReference.get(); + assertThat(thread).extracting("virtual").as("%s is virtual", thread).isEqualTo(true); + return thread.getName(); + } + @Configuration(proxyBeanMethods = false) static class CustomTaskExecutorBuilderConfig { diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc index 71c66a419add..543f0dba659d 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc @@ -1,6 +1,9 @@ [[features.task-execution-and-scheduling]] == Task Execution and Scheduling -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. +In the absence of an `Executor` bean in the context, Spring Boot auto-configures an `AsyncTaskExecutor`. +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. +Otherwise, it will be a `ThreadPoolTaskExecutor` with sensible defaults. +In either case, the auto-configured executor will be automatically used for asynchronous task execution (`@EnableAsync`) and Spring MVC asynchronous request processing. [TIP] ==== @@ -10,7 +13,7 @@ Depending on your target arrangement, you could change your `Executor` into a `T The auto-configured `TaskExecutorBuilder` allows you to easily create instances that reproduce what the auto-configuration does by default. ==== -The thread pool uses 8 core threads that can grow and shrink according to the load. +When a `ThreadPoolTaskExecutor` is auto-configured, the thread pool uses 8 core threads that can grow and shrink according to the load. Those default settings can be fine-tuned using the `spring.task.execution` namespace, as shown in the following example: [source,yaml,indent=0,subs="verbatim",configprops,configblocks]