From a2160f793b2fd9b13d3149123365d6f46b026cc6 Mon Sep 17 00:00:00 2001 From: Nhahan Date: Mon, 24 Nov 2025 23:40:49 +0900 Subject: [PATCH 1/2] Relax conditions to create HttpService client with RestClient Previously, HttpServiceClientAutoConfiguration used NotReactiveWebApplicationCondition, which prevented activation in reactive apps even when virtual threads were enabled. This commit removes the condition to follow suite with what was done in gh-48308 See gh-48274 Signed-off-by: Nhahan --- .../HttpServiceClientAutoConfiguration.java | 2 +- ...irtualThreadsExecutorEnabledCondition.java | 54 +++++++++++++++++++ ...tpServiceClientAutoConfigurationTests.java | 34 ++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) 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 60926d6fb21..0605da85fb3 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 @@ -46,7 +46,7 @@ import org.springframework.web.service.registry.HttpServiceProxyRegistry; @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 00000000000..0c0347d036b --- /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 11d0ca19bae..4b1f86beba0 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.mockito.ArgumentCaptor; 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.http.client.autoconfigure.imperative.ImperativeH 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.RestClient.Builder; 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 @@ class HttpServiceClientAutoConfigurationTests { .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 @@ class HttpServiceClientAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + @ImportHttpServices(types = ReactiveTestClient.class, clientType = ClientType.REST_CLIENT, group = "echo") + static class ReactiveHttpClientConfiguration { + + } + + interface ReactiveTestClient { + + @GetExchange("/echo") + String echo(); + + } + } From 4feb1faf3aa3f517b499318b2ad771969776ad6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 27 Nov 2025 09:54:26 +0100 Subject: [PATCH 2/2] Polish "Relax conditions to create HttpService client with RestClient" See gh-48274 --- .../HttpServiceClientAutoConfiguration.java | 2 - .../NotReactiveWebApplicationCondition.java | 41 -------------- ...irtualThreadsExecutorEnabledCondition.java | 54 ------------------- ...tpServiceClientAutoConfigurationTests.java | 34 ------------ 4 files changed, 131 deletions(-) delete mode 100644 module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/service/NotReactiveWebApplicationCondition.java delete 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 0605da85fb3..7f3118f8491 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 @@ -29,7 +29,6 @@ import org.springframework.boot.restclient.RestClientCustomizer; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; import org.springframework.core.io.ResourceLoader; import org.springframework.web.client.RestClient; import org.springframework.web.client.support.RestClientAdapter; @@ -46,7 +45,6 @@ import org.springframework.web.service.registry.HttpServiceProxyRegistry; @AutoConfiguration(after = { ImperativeHttpClientAutoConfiguration.class, RestClientAutoConfiguration.class }) @ConditionalOnClass(RestClientAdapter.class) @ConditionalOnBean(HttpServiceProxyRegistry.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/NotReactiveWebApplicationCondition.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/service/NotReactiveWebApplicationCondition.java deleted file mode 100644 index f25fba42c26..00000000000 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/service/NotReactiveWebApplicationCondition.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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.ConditionalOnWebApplication; -import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; -import org.springframework.boot.autoconfigure.condition.NoneNestedConditions; -import org.springframework.boot.autoconfigure.condition.SpringBootCondition; - -/** - * {@link SpringBootCondition} that applies only when running in a non-reactive web - * application. - * - * @author Phillip Webb - */ -class NotReactiveWebApplicationCondition extends NoneNestedConditions { - - NotReactiveWebApplicationCondition() { - super(ConfigurationPhase.PARSE_CONFIGURATION); - } - - @ConditionalOnWebApplication(type = Type.REACTIVE) - private static final class ReactiveWebApplication { - - } - -} 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 deleted file mode 100644 index 0c0347d036b..00000000000 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/autoconfigure/service/NotReactiveWebApplicationOrVirtualThreadsExecutorEnabledCondition.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 4b1f86beba0..11d0ca19bae 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,7 +35,6 @@ import org.mockito.ArgumentCaptor; 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; @@ -44,7 +43,6 @@ import org.springframework.boot.http.client.autoconfigure.imperative.ImperativeH 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; @@ -56,12 +54,10 @@ import org.springframework.web.client.RestClient.Builder; 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; @@ -221,23 +217,6 @@ class HttpServiceClientAutoConfigurationTests { .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)); } @@ -336,17 +315,4 @@ class HttpServiceClientAutoConfigurationTests { } - @Configuration(proxyBeanMethods = false) - @ImportHttpServices(types = ReactiveTestClient.class, clientType = ClientType.REST_CLIENT, group = "echo") - static class ReactiveHttpClientConfiguration { - - } - - interface ReactiveTestClient { - - @GetExchange("/echo") - String echo(); - - } - }