From 871f4a98232a08844e2d4e31dcfa6846d7c4c73e Mon Sep 17 00:00:00 2001 From: Nhahan Date: Mon, 24 Nov 2025 23:40:49 +0900 Subject: [PATCH 1/2] Support virtual threads in HTTP Service Client autoconfiguration Previously, HttpServiceClientAutoConfiguration used NotReactiveWebApplicationCondition, which prevented activation in reactive apps even when virtual threads were enabled. This commit updates the condition to NotReactiveWebApplicationOrVirtualThreadsExecutorEnabledCondition, allowing HTTP Service Clients to work in reactive apps when virtual threads are enabled, matching the behavior of RestClientAutoConfiguration. Closes gh-48273 Signed-off-by: Nhahan --- .../HttpServiceClientAutoConfiguration.java | 5 +- ...irtualThreadsExecutorEnabledCondition.java | 54 +++++++++++++++++++ ...tpServiceClientAutoConfigurationTests.java | 34 ++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/service/NotReactiveWebApplicationOrVirtualThreadsExecutorEnabledCondition.java diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/service/HttpServiceClientAutoConfiguration.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/service/HttpServiceClientAutoConfiguration.java index 60926d6fb210..80a9cba85755 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/service/HttpServiceClientAutoConfiguration.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/service/HttpServiceClientAutoConfiguration.java @@ -36,7 +36,8 @@ import org.springframework.web.service.registry.HttpServiceProxyRegistry; /** - * AutoConfiguration for Spring HTTP Service clients backed by {@link RestClient}. + * AutoConfiguration for Spring HTTP Service clients backed by + * {@link RestClient}. * * @author Olga Maciaszek-Sharma * @author Rossen Stoyanchev @@ -46,7 +47,7 @@ @AutoConfiguration(after = { ImperativeHttpClientAutoConfiguration.class, RestClientAutoConfiguration.class }) @ConditionalOnClass(RestClientAdapter.class) @ConditionalOnBean(HttpServiceProxyRegistry.class) -@Conditional(NotReactiveWebApplicationCondition.class) +@Conditional(NotReactiveWebApplicationOrVirtualThreadsExecutorEnabledCondition.class) @EnableConfigurationProperties(HttpServiceClientProperties.class) public final class HttpServiceClientAutoConfiguration { diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/service/NotReactiveWebApplicationOrVirtualThreadsExecutorEnabledCondition.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/service/NotReactiveWebApplicationOrVirtualThreadsExecutorEnabledCondition.java new file mode 100644 index 000000000000..0c0347d036bb --- /dev/null +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/service/NotReactiveWebApplicationOrVirtualThreadsExecutorEnabledCondition.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-present 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.restclient.autoconfigure.service; + +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; +import org.springframework.boot.thread.Threading; +import org.springframework.context.annotation.Conditional; + +/** + * {@link SpringBootCondition} that applies when running in a non-reactive web application + * or virtual threads are enabled. + * + * Package-private by design to avoid exposing conditions as public API. Should be kept in + * sync with + * {@code org.springframework.boot.restclient.autoconfigure.NotReactiveWebApplicationOrVirtualThreadsExecutorEnabledCondition}. + * + * @author Dmitry Sulman + */ +class NotReactiveWebApplicationOrVirtualThreadsExecutorEnabledCondition extends AnyNestedCondition { + + NotReactiveWebApplicationOrVirtualThreadsExecutorEnabledCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @Conditional(NotReactiveWebApplicationCondition.class) + private static final class NotReactiveWebApplication { + + } + + @ConditionalOnThreading(Threading.VIRTUAL) + @ConditionalOnBean(name = TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME) + private static final class VirtualThreadsExecutorEnabled { + + } + +} diff --git a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/service/HttpServiceClientAutoConfigurationTests.java b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/service/HttpServiceClientAutoConfigurationTests.java index 11d0ca19baef..4b1f86beba0c 100644 --- a/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/service/HttpServiceClientAutoConfigurationTests.java +++ b/module/spring-boot-restclient/src/test/java/org/springframework/boot/restclient/autoconfigure/service/HttpServiceClientAutoConfigurationTests.java @@ -35,6 +35,7 @@ import org.springframework.aop.Advisor; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; import org.springframework.boot.http.client.HttpClientSettings; import org.springframework.boot.http.client.HttpRedirects; @@ -43,6 +44,7 @@ import org.springframework.boot.restclient.RestClientCustomizer; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.ClientHttpRequestFactory; @@ -54,10 +56,12 @@ import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer; import org.springframework.web.service.annotation.GetExchange; import org.springframework.web.service.registry.HttpServiceGroup; +import org.springframework.web.service.registry.HttpServiceGroup.ClientType; import org.springframework.web.service.registry.HttpServiceGroupConfigurer.ClientCallback; import org.springframework.web.service.registry.HttpServiceGroupConfigurer.Groups; import org.springframework.web.service.registry.HttpServiceProxyRegistry; import org.springframework.web.service.registry.ImportHttpServices; +import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -217,6 +221,23 @@ void whenHasNoHttpServiceProxyRegistryBean() { .run((context) -> assertThat(context).doesNotHaveBean(HttpServiceProxyRegistry.class)); } + @Test + void restClientServiceClientsApplyPropertiesWhenReactiveWithVirtualThreads() { + new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(HttpServiceClientAutoConfiguration.class, + ImperativeHttpClientAutoConfiguration.class, RestClientAutoConfiguration.class, + TaskExecutionAutoConfiguration.class)) + .withPropertyValues("spring.threads.virtual.enabled=true", + "spring.http.serviceclient.echo.base-url=https://example.com") + .withUserConfiguration(ReactiveHttpClientConfiguration.class) + .run((context) -> { + RestClient restClient = getRestClient(context.getBean(ReactiveTestClient.class)); + UriComponentsBuilder baseUri = (UriComponentsBuilder) Extractors.byName("uriBuilderFactory.baseUri") + .apply(restClient); + assertThat(baseUri.build().toUriString()).isEqualTo("https://example.com"); + }); + } + private HttpClient getJdkHttpClient(Object proxy) { return (HttpClient) Extractors.byName("clientRequestFactory.httpClient").apply(getRestClient(proxy)); } @@ -315,4 +336,17 @@ interface TestClientTwo { } + @Configuration(proxyBeanMethods = false) + @ImportHttpServices(types = ReactiveTestClient.class, clientType = ClientType.REST_CLIENT, group = "echo") + static class ReactiveHttpClientConfiguration { + + } + + interface ReactiveTestClient { + + @GetExchange("/echo") + String echo(); + + } + } From 42656898e5b660a8ace601e38b79dec588b9d01a Mon Sep 17 00:00:00 2001 From: Nhahan Date: Tue, 25 Nov 2025 00:14:42 +0900 Subject: [PATCH 2/2] Fix RestClient HTTP service condition for reactive VT Signed-off-by: Nhahan --- ...WebApplicationOrVirtualThreadsExecutorEnabledCondition.java | 3 +++ .../service/HttpServiceClientAutoConfiguration.java | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/NotReactiveWebApplicationOrVirtualThreadsExecutorEnabledCondition.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/NotReactiveWebApplicationOrVirtualThreadsExecutorEnabledCondition.java index e9b372242781..edbc2f35ae43 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/NotReactiveWebApplicationOrVirtualThreadsExecutorEnabledCondition.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/NotReactiveWebApplicationOrVirtualThreadsExecutorEnabledCondition.java @@ -28,6 +28,9 @@ * {@link SpringBootCondition} that applies when running in a non-reactive web application * or virtual threads are enabled. * + * Should be kept in sync with + * {@code org.springframework.boot.restclient.autoconfigure.service.NotReactiveWebApplicationOrVirtualThreadsExecutorEnabledCondition}. + * * @author Dmitry Sulman */ class NotReactiveWebApplicationOrVirtualThreadsExecutorEnabledCondition extends AnyNestedCondition { diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/service/HttpServiceClientAutoConfiguration.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/service/HttpServiceClientAutoConfiguration.java index 80a9cba85755..0605da85fb31 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/service/HttpServiceClientAutoConfiguration.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/service/HttpServiceClientAutoConfiguration.java @@ -36,8 +36,7 @@ import org.springframework.web.service.registry.HttpServiceProxyRegistry; /** - * AutoConfiguration for Spring HTTP Service clients backed by - * {@link RestClient}. + * AutoConfiguration for Spring HTTP Service clients backed by {@link RestClient}. * * @author Olga Maciaszek-Sharma * @author Rossen Stoyanchev