From d205d10519e3a46010e7611b50ea6b47b584464a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 18 Jul 2023 10:18:37 +0100 Subject: [PATCH] Configure WebFlux's blocking execution to use applicationTaskExecutor Closes gh-36331 --- .../reactive/WebFluxAutoConfiguration.java | 14 ++++ .../WebFluxAutoConfigurationTests.java | 77 +++++++++++++++++++ .../task-execution-and-scheduling.adoc | 6 +- 3 files changed, 94 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java index 54b1effcb9..c3d77d94fa 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java @@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; @@ -54,12 +55,14 @@ import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.format.FormatterRegistry; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.util.ClassUtils; import org.springframework.validation.Validator; import org.springframework.web.filter.reactive.HiddenHttpMethodFilter; +import org.springframework.web.reactive.config.BlockingExecutionConfigurer; import org.springframework.web.reactive.config.DelegatingWebFluxConfiguration; import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.reactive.config.ResourceHandlerRegistration; @@ -184,6 +187,17 @@ public class WebFluxAutoConfiguration { this.codecCustomizers.orderedStream().forEach((customizer) -> customizer.customize(configurer)); } + @Override + public void configureBlockingExecution(BlockingExecutionConfigurer configurer) { + if (this.beanFactory.containsBean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)) { + Object taskExecutor = this.beanFactory + .getBean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME); + if (taskExecutor instanceof AsyncTaskExecutor asyncTaskExecutor) { + configurer.setExecutor(asyncTaskExecutor); + } + } + } + @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!this.resourceProperties.isAddMappings()) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java index 15759a908e..65be8c0f79 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java @@ -28,6 +28,7 @@ import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -43,6 +44,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.aop.support.AopUtils; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; import org.springframework.boot.autoconfigure.web.ServerProperties; @@ -62,6 +64,7 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.core.convert.ConversionService; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.format.Parser; import org.springframework.format.Printer; import org.springframework.format.support.FormattingConversionService; @@ -79,6 +82,7 @@ import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.filter.reactive.HiddenHttpMethodFilter; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.reactive.config.BlockingExecutionConfigurer; import org.springframework.web.reactive.config.DelegatingWebFluxConfiguration; import org.springframework.web.reactive.config.WebFluxConfigurationSupport; import org.springframework.web.reactive.config.WebFluxConfigurer; @@ -667,6 +671,47 @@ class WebFluxAutoConfigurationTests { .hasSingleBean(CustomExceptionHandler.class)); } + @Test + void asyncTaskExecutorWithApplicationTaskExecutor() { + this.contextRunner.withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)) + .run((context) -> { + assertThat(context).hasSingleBean(AsyncTaskExecutor.class); + assertThat(context.getBean(RequestMappingHandlerAdapter.class)).extracting("scheduler.executor") + .isSameAs(context.getBean("applicationTaskExecutor")); + }); + } + + @Test + void asyncTaskExecutorWithNonMatchApplicationTaskExecutorBean() { + this.contextRunner.withUserConfiguration(CustomApplicationTaskExecutorConfig.class) + .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)) + .run((context) -> { + assertThat(context).doesNotHaveBean(AsyncTaskExecutor.class); + assertThat(context.getBean(RequestMappingHandlerAdapter.class)).extracting("scheduler.executor") + .isNotSameAs(context.getBean("applicationTaskExecutor")); + }); + } + + @Test + void asyncTaskExecutorWithWebFluxConfigurerCanOverrideExecutor() { + this.contextRunner.withUserConfiguration(CustomAsyncTaskExecutorConfigurer.class) + .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)) + .run((context) -> assertThat(context.getBean(RequestMappingHandlerAdapter.class)) + .extracting("scheduler.executor") + .isSameAs(context.getBean(CustomAsyncTaskExecutorConfigurer.class).taskExecutor)); + } + + @Test + void asyncTaskExecutorWithCustomNonApplicationTaskExecutor() { + this.contextRunner.withUserConfiguration(CustomAsyncTaskExecutorConfig.class) + .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)) + .run((context) -> { + assertThat(context).hasSingleBean(AsyncTaskExecutor.class); + assertThat(context.getBean(RequestMappingHandlerAdapter.class)).extracting("scheduler.executor") + .isNotSameAs(context.getBean("customTaskExecutor")); + }); + } + private ContextConsumer assertExchangeWithSession( Consumer exchange) { return (context) -> { @@ -981,4 +1026,36 @@ class WebFluxAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class CustomApplicationTaskExecutorConfig { + + @Bean + Executor applicationTaskExecutor() { + return mock(Executor.class); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomAsyncTaskExecutorConfig { + + @Bean + AsyncTaskExecutor customTaskExecutor() { + return mock(AsyncTaskExecutor.class); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomAsyncTaskExecutorConfigurer implements WebFluxConfigurer { + + private final AsyncTaskExecutor taskExecutor = mock(AsyncTaskExecutor.class); + + @Override + public void configureBlockingExecution(BlockingExecutionConfigurer configurer) { + configurer.setExecutor(this.taskExecutor); + } + + } + } 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 543f0dba65..5eb6f53468 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 @@ -3,12 +3,12 @@ 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. +In either case, the auto-configured executor will be automatically used for asynchronous task execution (`@EnableAsync`), Spring MVC asynchronous request processing, and Spring WebFlux blocking execution. [TIP] ==== -If you have defined a custom `Executor` in the context, regular task execution (that is `@EnableAsync`) will use it transparently but the Spring MVC support will not be configured as it requires an `AsyncTaskExecutor` implementation (named `applicationTaskExecutor`). -Depending on your target arrangement, you could change your `Executor` into a `ThreadPoolTaskExecutor` or define both a `ThreadPoolTaskExecutor` and an `AsyncConfigurer` wrapping your custom `Executor`. +If you have defined a custom `Executor` in the context, regular task execution (that is `@EnableAsync`) will use it transparently but the Spring MVC and Spring WebFlux support will not be configured as they require an `AsyncTaskExecutor` implementation (named `applicationTaskExecutor`). +Depending on your target arrangement, you could change your `Executor` into an `AsyncTaskExecutor` or define both an `AsyncTaskExecutor` and an `AsyncConfigurer` wrapping your custom `Executor`. The auto-configured `TaskExecutorBuilder` allows you to easily create instances that reproduce what the auto-configuration does by default. ====