From 5061f738be23868f5623c46e2923a44ebb2bec98 Mon Sep 17 00:00:00 2001 From: Sangmin Park Date: Fri, 11 Jul 2025 14:14:02 +0900 Subject: [PATCH 1/2] Refactor ReactorEnvironmentPostProcessor to use Threading for virtual thread support Signed-off-by: Sangmin Park --- .../boot/reactor/ReactorEnvironmentPostProcessor.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/module/spring-boot-reactor/src/main/java/org/springframework/boot/reactor/ReactorEnvironmentPostProcessor.java b/module/spring-boot-reactor/src/main/java/org/springframework/boot/reactor/ReactorEnvironmentPostProcessor.java index 41110ccddf53..2e3f183fc3c6 100644 --- a/module/spring-boot-reactor/src/main/java/org/springframework/boot/reactor/ReactorEnvironmentPostProcessor.java +++ b/module/spring-boot-reactor/src/main/java/org/springframework/boot/reactor/ReactorEnvironmentPostProcessor.java @@ -17,8 +17,8 @@ package org.springframework.boot.reactor; import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.thread.Threading; import org.springframework.boot.env.EnvironmentPostProcessor; -import org.springframework.boot.system.JavaVersion; import org.springframework.core.Ordered; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.util.ClassUtils; @@ -56,8 +56,7 @@ public void postProcessEnvironment(ConfigurableEnvironment environment, SpringAp } } } - if (environment.getProperty("spring.threads.virtual.enabled", boolean.class, false) - && JavaVersion.getJavaVersion().isEqualOrNewerThan(JavaVersion.TWENTY_ONE)) { + if (Threading.VIRTUAL.isActive(environment)) { System.setProperty("reactor.schedulers.defaultBoundedElasticOnVirtualThreads", "true"); } } From 74b91973bb70b920f8e6f49e1c368a0970be1951 Mon Sep 17 00:00:00 2001 From: Sangmin Park Date: Fri, 11 Jul 2025 14:14:36 +0900 Subject: [PATCH 2/2] Add support for virtual and platform threading in HttpClient configuration Signed-off-by: Sangmin Park --- .../JdkClientHttpRequestFactoryBuilder.java | 7 +++++++ .../HttpClientAutoConfiguration.java | 20 +++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilder.java index 03cc4a4b1fed..0226925e65bd 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilder.java @@ -19,6 +19,7 @@ import java.net.http.HttpClient; import java.util.Collection; import java.util.List; +import java.util.concurrent.Executors; import java.util.function.Consumer; import org.springframework.boot.context.properties.PropertyMapper; @@ -73,6 +74,12 @@ public JdkClientHttpRequestFactoryBuilder withHttpClientCustomizer( this.httpClientBuilder.withCustomizer(httpClientCustomizer)); } + public JdkClientHttpRequestFactoryBuilder enableVirtualThreadExecutor() { + return this.withHttpClientCustomizer(builder -> + builder.executor(Executors.newVirtualThreadPerTaskExecutor()) + ); + } + @Override protected JdkClientHttpRequestFactory createClientHttpRequestFactory(ClientHttpRequestFactorySettings settings) { HttpClient httpClient = this.httpClientBuilder.build(asHttpClientSettings(settings.withReadTimeout(null))); diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/autoconfigure/HttpClientAutoConfiguration.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/autoconfigure/HttpClientAutoConfiguration.java index cb78b816aa02..f19e803aa4a3 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/autoconfigure/HttpClientAutoConfiguration.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/autoconfigure/HttpClientAutoConfiguration.java @@ -24,10 +24,13 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading; import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; +import org.springframework.boot.autoconfigure.thread.Threading; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; import org.springframework.boot.http.client.ClientHttpRequestFactorySettings; +import org.springframework.boot.http.client.JdkClientHttpRequestFactoryBuilder; import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.util.LambdaSafe; import org.springframework.context.annotation.Bean; @@ -63,18 +66,31 @@ public void setBeanClassLoader(ClassLoader classLoader) { @Bean @ConditionalOnMissingBean - ClientHttpRequestFactoryBuilder clientHttpRequestFactoryBuilder( + @ConditionalOnThreading(Threading.PLATFORM) + ClientHttpRequestFactoryBuilder clientHttpRequestFactoryBuilderOnPlatform( ObjectProvider> clientHttpRequestFactoryBuilderCustomizers) { ClientHttpRequestFactoryBuilder builder = this.factories.builder(this.beanClassLoader); return customize(builder, clientHttpRequestFactoryBuilderCustomizers.orderedStream().toList()); } + @Bean + @ConditionalOnMissingBean + @ConditionalOnThreading(Threading.VIRTUAL) + ClientHttpRequestFactoryBuilder clientHttpRequestFactoryBuilderOnVirtual( + ObjectProvider> clientHttpRequestFactoryBuilderCustomizers) { + ClientHttpRequestFactoryBuilder builder = this.factories.builder(this.beanClassLoader); + if (builder instanceof JdkClientHttpRequestFactoryBuilder jdk) { + return customize(jdk.enableVirtualThreadExecutor(), clientHttpRequestFactoryBuilderCustomizers.orderedStream().toList()); + } + return customize(builder, clientHttpRequestFactoryBuilderCustomizers.orderedStream().toList()); + } + @SuppressWarnings("unchecked") private ClientHttpRequestFactoryBuilder customize(ClientHttpRequestFactoryBuilder builder, List> customizers) { ClientHttpRequestFactoryBuilder[] builderReference = { builder }; LambdaSafe.callbacks(ClientHttpRequestFactoryBuilderCustomizer.class, customizers, builderReference[0]) - .invoke((customizer) -> builderReference[0] = customizer.customize(builderReference[0])); + .invoke((customizer) -> builderReference[0] = customizer.customize(builderReference[0])); return builderReference[0]; }