Add `@HttpServiceClient` scanning auto-configuration

Refactor `HttpServiceClientAutoConfiguration` and
`ReactiveHttpServiceClientAutoConfiguration` to support scanning for
`@HttpServiceClient` annotated interfaces.

Closes gh-46782
This commit is contained in:
Phillip Webb 2025-08-11 17:38:48 +01:00
parent 11c5a8c404
commit 7a8b337b1c
14 changed files with 600 additions and 88 deletions

View File

@ -0,0 +1,40 @@
/*
* 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.http.client.autoconfigure.service;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Conditional;
/**
* {@link Conditional @Conditional} that matches when one or more HTTP Service bean has
* been registered.
*
* @author Phillip Webb@
* @since 4.0.0
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnMissingHttpServiceProxyBeanCondition.class)
public @interface ConditionalOnMissingHttpServiceProxyBean {
}

View File

@ -0,0 +1,73 @@
/*
* 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.http.client.autoconfigure.service;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.HierarchicalBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnJava;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.ConfigurationCondition;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* {@link Condition} that checks for any HTTP Service proxy bean.
*
* @author Phillip Webb
* @see ConditionalOnJava
*/
class OnMissingHttpServiceProxyBeanCondition extends SpringBootCondition implements ConfigurationCondition {
static final String HTTP_SERVICE_GROUP_NAME_ATTRIBUTE = "httpServiceGroupName";
@Override
public ConfigurationPhase getConfigurationPhase() {
return ConfigurationPhase.REGISTER_BEAN;
}
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage
.forCondition(ConditionalOnMissingHttpServiceProxyBean.class);
BeanFactory beanFactory = context.getBeanFactory();
while (beanFactory != null) {
if (beanFactory instanceof ConfigurableListableBeanFactory configurableListableBeanFactory
&& hasHttpServiceProxyBeanDefinition(configurableListableBeanFactory)) {
return ConditionOutcome.noMatch(message.foundExactly("HTTP Service proxy bean"));
}
beanFactory = (beanFactory instanceof HierarchicalBeanFactory hierarchicalBeanFactory)
? hierarchicalBeanFactory.getParentBeanFactory() : null;
}
return ConditionOutcome.match(message.didNotFind("").items("HTTP Service proxy beans"));
}
private boolean hasHttpServiceProxyBeanDefinition(ConfigurableListableBeanFactory beanFactory) {
for (String beanName : beanFactory.getBeanDefinitionNames()) {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
if (beanDefinition.hasAttribute(HTTP_SERVICE_GROUP_NAME_ATTRIBUTE)) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,23 @@
/*
* 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.
*/
/**
* Common support code for HTTP Service Clients.
*/
@NullMarked
package org.springframework.boot.http.client.autoconfigure.service;
import org.jspecify.annotations.NullMarked;

View File

@ -0,0 +1,82 @@
/*
* 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.http.client.autoconfigure.service;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.registry.AbstractHttpServiceRegistrar;
import org.springframework.web.service.registry.ImportHttpServices;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for
* {@link ConditionalOnMissingHttpServiceProxyBean @ConditionalOnMissingHttpServiceProxyBean}.
*
* @author Phillip Webb
*/
class ConditionalOnMissingHttpServiceProxyBeanTests {
@Test
void attributeNameMatchesSpringFramework() {
assertThat(OnMissingHttpServiceProxyBeanCondition.HTTP_SERVICE_GROUP_NAME_ATTRIBUTE).isEqualTo(
ReflectionTestUtils.getField(AbstractHttpServiceRegistrar.class, "HTTP_SERVICE_GROUP_NAME_ATTRIBUTE"));
}
@Test
void getOutcomeWhenNoHttpServiceProxyMatches() {
new ApplicationContextRunner().withUserConfiguration(TestConfiguration.class)
.run((context) -> assertThat(context).hasBean("test"));
}
@Test
void getOutcomeWhenHasHttpServiceProxyDoesNotMatch() {
new ApplicationContextRunner()
.withUserConfiguration(HttpServiceProxyConfiguration.class, TestConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(TestHttpService.class).doesNotHaveBean("test"));
}
@Configuration(proxyBeanMethods = false)
@ImportHttpServices(TestHttpService.class)
static class HttpServiceProxyConfiguration {
}
@Configuration(proxyBeanMethods = false)
static class TestConfiguration {
@Bean
@ConditionalOnMissingHttpServiceProxyBean
String test() {
return "test";
}
}
interface TestHttpService {
@GetExchange("/test")
String test();
}
}

View File

@ -16,25 +16,14 @@
package org.springframework.boot.restclient.autoconfigure.service;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
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.autoconfigure.HttpClientAutoConfiguration;
import org.springframework.boot.http.client.autoconfigure.HttpClientProperties;
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.web.client.ApiVersionFormatter;
import org.springframework.web.client.ApiVersionInserter;
import org.springframework.context.annotation.Import;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.registry.HttpServiceProxyRegistry;
import org.springframework.web.service.registry.ImportHttpServices;
/**
@ -50,39 +39,9 @@ import org.springframework.web.service.registry.ImportHttpServices;
*/
@AutoConfiguration(after = { HttpClientAutoConfiguration.class, RestClientAutoConfiguration.class })
@ConditionalOnClass(RestClientAdapter.class)
@ConditionalOnBean(HttpServiceProxyRegistry.class)
@Conditional(NotReactiveWebApplicationCondition.class)
@EnableConfigurationProperties(HttpClientServiceProperties.class)
public final class HttpServiceClientAutoConfiguration implements BeanClassLoaderAware {
@SuppressWarnings("NullAway.Init")
private ClassLoader beanClassLoader;
HttpServiceClientAutoConfiguration() {
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.beanClassLoader = classLoader;
}
@Bean
RestClientPropertiesHttpServiceGroupConfigurer restClientPropertiesHttpServiceGroupConfigurer(
ObjectProvider<SslBundles> sslBundles, ObjectProvider<HttpClientProperties> httpClientProperties,
HttpClientServiceProperties serviceProperties,
ObjectProvider<ClientHttpRequestFactoryBuilder<?>> clientFactoryBuilder,
ObjectProvider<ClientHttpRequestFactorySettings> clientHttpRequestFactorySettings,
ObjectProvider<ApiVersionInserter> apiVersionInserter,
ObjectProvider<ApiVersionFormatter> apiVersionFormatter) {
return new RestClientPropertiesHttpServiceGroupConfigurer(this.beanClassLoader, sslBundles,
httpClientProperties.getIfAvailable(), serviceProperties, clientFactoryBuilder,
clientHttpRequestFactorySettings, apiVersionInserter, apiVersionFormatter);
}
@Bean
RestClientCustomizerHttpServiceGroupConfigurer restClientCustomizerHttpServiceGroupConfigurer(
ObjectProvider<RestClientCustomizer> customizers) {
return new RestClientCustomizerHttpServiceGroupConfigurer(customizers);
}
@Import({ ImportHttpServiceClientsConfiguration.class, RestClientHttpServiceClientConfiguration.class })
public final class HttpServiceClientAutoConfiguration {
}

View File

@ -0,0 +1,62 @@
/*
* 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.beans.factory.BeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.boot.http.client.autoconfigure.service.ConditionalOnMissingHttpServiceProxyBean;
import org.springframework.boot.restclient.autoconfigure.service.ImportHttpServiceClientsConfiguration.ImportHttpServiceClients;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.web.service.registry.AbstractClientHttpServiceRegistrar;
import org.springframework.web.service.registry.HttpServiceClient;
/**
* {@link Configuration @Configuration} to import {@link ImportHttpServiceClients} when no
* user-defined HTTP service client beans are found.
*
* @author Phillip Webb
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingHttpServiceProxyBean
@Import(ImportHttpServiceClients.class)
class ImportHttpServiceClientsConfiguration {
/**
* {@link AbstractClientHttpServiceRegistrar} to import
* {@link HttpServiceClient @HttpServiceClient} annotated classes from
* {@link AutoConfigurationPackages}.
*/
static class ImportHttpServiceClients extends AbstractClientHttpServiceRegistrar {
private final BeanFactory beanFactory;
ImportHttpServiceClients(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Override
protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata importingClassMetadata) {
if (AutoConfigurationPackages.has(this.beanFactory)) {
findAndRegisterHttpServiceClients(registry, AutoConfigurationPackages.get(this.beanFactory));
}
}
}
}

View File

@ -0,0 +1,73 @@
/*
* 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.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
import org.springframework.boot.http.client.autoconfigure.HttpClientProperties;
import org.springframework.boot.restclient.RestClientCustomizer;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.ApiVersionFormatter;
import org.springframework.web.client.ApiVersionInserter;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
import org.springframework.web.service.registry.HttpServiceProxyRegistry;
/**
* {@link Configuration @Configuration} to register
* {@link RestClientHttpServiceGroupConfigurer} beans to support HTTP service clients
* backed by a {@link RestClient}.
*
* @author Phillip Webb
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(HttpServiceProxyRegistry.class)
class RestClientHttpServiceClientConfiguration implements BeanClassLoaderAware {
@SuppressWarnings("NullAway.Init")
private ClassLoader beanClassLoader;
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.beanClassLoader = classLoader;
}
@Bean
RestClientPropertiesHttpServiceGroupConfigurer restClientPropertiesHttpServiceGroupConfigurer(
ObjectProvider<SslBundles> sslBundles, ObjectProvider<HttpClientProperties> httpClientProperties,
HttpClientServiceProperties serviceProperties,
ObjectProvider<ClientHttpRequestFactoryBuilder<?>> clientFactoryBuilder,
ObjectProvider<ClientHttpRequestFactorySettings> clientHttpRequestFactorySettings,
ObjectProvider<ApiVersionInserter> apiVersionInserter,
ObjectProvider<ApiVersionFormatter> apiVersionFormatter) {
return new RestClientPropertiesHttpServiceGroupConfigurer(this.beanClassLoader, sslBundles,
httpClientProperties.getIfAvailable(), serviceProperties, clientFactoryBuilder,
clientHttpRequestFactorySettings, apiVersionInserter, apiVersionFormatter);
}
@Bean
RestClientCustomizerHttpServiceGroupConfigurer restClientCustomizerHttpServiceGroupConfigurer(
ObjectProvider<RestClientCustomizer> customizers) {
return new RestClientCustomizerHttpServiceGroupConfigurer(customizers);
}
}

View File

@ -27,6 +27,7 @@ import org.assertj.core.extractor.Extractors;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Advisor;
import org.springframework.boot.autoconfigure.AutoConfigurationPackage;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
@ -34,6 +35,7 @@ import org.springframework.boot.http.client.HttpRedirects;
import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration;
import org.springframework.boot.restclient.RestClientCustomizer;
import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration;
import org.springframework.boot.restclient.autoconfigure.service.scan.TestHttpServiceClient;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -155,6 +157,18 @@ class HttpServiceClientAutoConfigurationTests {
.run((context) -> assertThat(context).doesNotHaveBean(HttpServiceProxyRegistry.class));
}
@Test
void registerHttpServiceAnnotatedInterfacesInPackages() {
this.contextRunner.withUserConfiguration(ScanConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(TestHttpServiceClient.class));
}
@Test
void whenHasImportAnnotationDoesNotRegisterHttpServiceAnnotatedInterfacesInPackages() {
this.contextRunner.withUserConfiguration(ScanConfiguration.class, HttpClientConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean(TestHttpServiceClient.class));
}
private HttpClient getJdkHttpClient(Object proxy) {
return (HttpClient) Extractors.byName("clientRequestFactory.httpClient").apply(getRestClient(proxy));
}
@ -237,6 +251,12 @@ class HttpServiceClientAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
@AutoConfigurationPackage(basePackageClasses = TestHttpServiceClient.class)
static class ScanConfiguration {
}
interface TestClientOne {
@GetExchange("/hello")

View File

@ -0,0 +1,33 @@
/*
* 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.scan;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.registry.HttpServiceClient;
/**
* Test HTTP service used with scanning.
*
* @author Phillip Webb
*/
@HttpServiceClient("test")
public interface TestHttpServiceClient {
@GetExchange("/hello")
String hello();
}

View File

@ -0,0 +1,62 @@
/*
* 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.webclient.autoconfigure.service;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.boot.http.client.autoconfigure.service.ConditionalOnMissingHttpServiceProxyBean;
import org.springframework.boot.webclient.autoconfigure.service.ImportHttpServiceClientsConfiguration.ImportHttpServiceClients;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.web.service.registry.AbstractClientHttpServiceRegistrar;
import org.springframework.web.service.registry.HttpServiceClient;
/**
* {@link Configuration @Configuration} to import {@link ImportHttpServiceClients} when no
* user-defined HTTP service client beans are found.
*
* @author Phillip Webb
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingHttpServiceProxyBean
@Import(ImportHttpServiceClients.class)
class ImportHttpServiceClientsConfiguration {
/**
* {@link AbstractClientHttpServiceRegistrar} to import
* {@link HttpServiceClient @HttpServiceClient} annotated classes from
* {@link AutoConfigurationPackages}.
*/
static class ImportHttpServiceClients extends AbstractClientHttpServiceRegistrar {
private final BeanFactory beanFactory;
ImportHttpServiceClients(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Override
protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata importingClassMetadata) {
if (AutoConfigurationPackages.has(this.beanFactory)) {
findAndRegisterHttpServiceClients(registry, AutoConfigurationPackages.get(this.beanFactory));
}
}
}
}

View File

@ -16,24 +16,13 @@
package org.springframework.boot.webclient.autoconfigure.service;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.http.client.autoconfigure.reactive.ClientHttpConnectorAutoConfiguration;
import org.springframework.boot.http.client.autoconfigure.reactive.HttpReactiveClientProperties;
import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder;
import org.springframework.boot.http.client.reactive.ClientHttpConnectorSettings;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.webclient.WebClientCustomizer;
import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.ApiVersionFormatter;
import org.springframework.web.client.ApiVersionInserter;
import org.springframework.context.annotation.Import;
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
import org.springframework.web.service.registry.HttpServiceProxyRegistry;
import org.springframework.web.service.registry.ImportHttpServices;
/**
@ -49,38 +38,8 @@ import org.springframework.web.service.registry.ImportHttpServices;
*/
@AutoConfiguration(after = { ClientHttpConnectorAutoConfiguration.class, WebClientAutoConfiguration.class })
@ConditionalOnClass(WebClientAdapter.class)
@ConditionalOnBean(HttpServiceProxyRegistry.class)
@EnableConfigurationProperties(ReactiveHttpClientServiceProperties.class)
public final class ReactiveHttpServiceClientAutoConfiguration implements BeanClassLoaderAware {
@SuppressWarnings("NullAway.Init")
private ClassLoader beanClassLoader;
ReactiveHttpServiceClientAutoConfiguration() {
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.beanClassLoader = classLoader;
}
@Bean
WebClientPropertiesHttpServiceGroupConfigurer webClientPropertiesHttpServiceGroupConfigurer(
ObjectProvider<SslBundles> sslBundles, HttpReactiveClientProperties httpReactiveClientProperties,
ReactiveHttpClientServiceProperties serviceProperties,
ObjectProvider<ClientHttpConnectorBuilder<?>> clientConnectorBuilder,
ObjectProvider<ClientHttpConnectorSettings> clientConnectorSettings,
ObjectProvider<ApiVersionInserter> apiVersionInserter,
ObjectProvider<ApiVersionFormatter> apiVersionFormatter) {
return new WebClientPropertiesHttpServiceGroupConfigurer(this.beanClassLoader, sslBundles,
httpReactiveClientProperties, serviceProperties, clientConnectorBuilder, clientConnectorSettings,
apiVersionInserter, apiVersionFormatter);
}
@Bean
WebClientCustomizerHttpServiceGroupConfigurer webClientCustomizerHttpServiceGroupConfigurer(
ObjectProvider<WebClientCustomizer> customizers) {
return new WebClientCustomizerHttpServiceGroupConfigurer(customizers);
}
@Import({ ImportHttpServiceClientsConfiguration.class, WebClientHttpServiceClientConfiguration.class })
public final class ReactiveHttpServiceClientAutoConfiguration {
}

View File

@ -0,0 +1,73 @@
/*
* 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.webclient.autoconfigure.service;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.http.client.autoconfigure.reactive.HttpReactiveClientProperties;
import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder;
import org.springframework.boot.http.client.reactive.ClientHttpConnectorSettings;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.webclient.WebClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.ApiVersionFormatter;
import org.springframework.web.client.ApiVersionInserter;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientHttpServiceGroupConfigurer;
import org.springframework.web.service.registry.HttpServiceProxyRegistry;
/**
* {@link Configuration @Configuration} to register
* {@link WebClientHttpServiceGroupConfigurer} beans to support HTTP service clients
* backed by a {@link WebClient}.
*
* @author Phillip Webb
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(HttpServiceProxyRegistry.class)
final class WebClientHttpServiceClientConfiguration implements BeanClassLoaderAware {
@SuppressWarnings("NullAway.Init")
private ClassLoader beanClassLoader;
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.beanClassLoader = classLoader;
}
@Bean
WebClientPropertiesHttpServiceGroupConfigurer webClientPropertiesHttpServiceGroupConfigurer(
ObjectProvider<SslBundles> sslBundles, HttpReactiveClientProperties httpReactiveClientProperties,
ReactiveHttpClientServiceProperties serviceProperties,
ObjectProvider<ClientHttpConnectorBuilder<?>> clientConnectorBuilder,
ObjectProvider<ClientHttpConnectorSettings> clientConnectorSettings,
ObjectProvider<ApiVersionInserter> apiVersionInserter,
ObjectProvider<ApiVersionFormatter> apiVersionFormatter) {
return new WebClientPropertiesHttpServiceGroupConfigurer(this.beanClassLoader, sslBundles,
httpReactiveClientProperties, serviceProperties, clientConnectorBuilder, clientConnectorSettings,
apiVersionInserter, apiVersionFormatter);
}
@Bean
WebClientCustomizerHttpServiceGroupConfigurer webClientCustomizerHttpServiceGroupConfigurer(
ObjectProvider<WebClientCustomizer> customizers) {
return new WebClientCustomizerHttpServiceGroupConfigurer(customizers);
}
}

View File

@ -27,6 +27,7 @@ import org.assertj.core.extractor.Extractors;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Advisor;
import org.springframework.boot.autoconfigure.AutoConfigurationPackage;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.http.client.HttpRedirects;
import org.springframework.boot.http.client.autoconfigure.reactive.ClientHttpConnectorAutoConfiguration;
@ -35,6 +36,7 @@ import org.springframework.boot.http.client.reactive.ClientHttpConnectorSettings
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.webclient.WebClientCustomizer;
import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration;
import org.springframework.boot.webclient.autoconfigure.service.scan.TestHttpServiceClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
@ -137,6 +139,18 @@ class ReactiveHttpServiceClientAutoConfigurationTests {
.run((context) -> assertThat(context).doesNotHaveBean(HttpServiceProxyRegistry.class));
}
@Test
void registerHttpServiceAnnotatedInterfacesInPackages() {
this.contextRunner.withUserConfiguration(ScanConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(TestHttpServiceClient.class));
}
@Test
void whenHasImportAnnotationDoesNotRegisterHttpServiceAnnotatedInterfacesInPackages() {
this.contextRunner.withUserConfiguration(ScanConfiguration.class, HttpClientConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean(TestHttpServiceClient.class));
}
private HttpClient getJdkHttpClient(Object proxy) {
return (HttpClient) Extractors.byName("builder.connector.httpClient").apply(getWebClient(proxy));
}
@ -206,6 +220,12 @@ class ReactiveHttpServiceClientAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
@AutoConfigurationPackage(basePackageClasses = TestHttpServiceClient.class)
static class ScanConfiguration {
}
interface TestClientOne {
@GetExchange("/hello")

View File

@ -0,0 +1,33 @@
/*
* 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.webclient.autoconfigure.service.scan;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.registry.HttpServiceClient;
/**
* Test HTTP service used with scanning.
*
* @author Phillip Webb
*/
@HttpServiceClient("test")
public interface TestHttpServiceClient {
@GetExchange("/hello")
String hello();
}