Rework HTTP client modules

This commit is contained in:
Andy Wilkinson
2025-05-13 12:49:28 +01:00
committed by Phillip Webb
parent 4763ef2463
commit 7a9be5bd4a
230 changed files with 371 additions and 1330 deletions

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2012-2019 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;
import org.springframework.web.reactive.function.client.WebClient;
/**
* Callback interface that can be used to customize a
* {@link org.springframework.web.reactive.function.client.WebClient.Builder
* WebClient.Builder}.
*
* @author Brian Clozel
* @since 2.0.0
*/
@FunctionalInterface
public interface WebClientCustomizer {
/**
* Callback to customize a
* {@link org.springframework.web.reactive.function.client.WebClient.Builder
* WebClient.Builder} instance.
* @param webClientBuilder the client builder to customize
*/
void customize(WebClient.Builder webClientBuilder);
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2012-2025 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.actuate.observation;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.boot.webclient.WebClientCustomizer;
import org.springframework.web.reactive.function.client.ClientRequestObservationConvention;
import org.springframework.web.reactive.function.client.WebClient;
/**
* {@link WebClientCustomizer} that configures the {@link WebClient} to record request
* observations.
*
* @author Brian Clozel
* @since 4.0.0
*/
public class ObservationWebClientCustomizer implements WebClientCustomizer {
private final ObservationRegistry observationRegistry;
private final ClientRequestObservationConvention observationConvention;
/**
* Create a new {@code ObservationWebClientCustomizer} that will configure the
* {@code Observation} setup on the client.
* @param observationRegistry the registry to publish observations to
* @param observationConvention the convention to use to populate observations
*/
public ObservationWebClientCustomizer(ObservationRegistry observationRegistry,
ClientRequestObservationConvention observationConvention) {
this.observationRegistry = observationRegistry;
this.observationConvention = observationConvention;
}
@Override
public void customize(WebClient.Builder webClientBuilder) {
webClientBuilder.observationRegistry(this.observationRegistry)
.observationConvention(this.observationConvention);
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
*/
/**
* Observation integration for WebClient.
*/
package org.springframework.boot.webclient.actuate.observation;

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2012-2025 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;
import java.util.function.Consumer;
import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder;
import org.springframework.boot.http.client.reactive.ClientHttpConnectorSettings;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
/**
* An auto-configured {@link WebClientSsl} implementation.
*
* @author Phillip Webb
*/
class AutoConfiguredWebClientSsl implements WebClientSsl {
private final ClientHttpConnectorBuilder<?> connectorBuilder;
private final ClientHttpConnectorSettings settings;
private final SslBundles sslBundles;
AutoConfiguredWebClientSsl(ClientHttpConnectorBuilder<?> connectorBuilder, ClientHttpConnectorSettings settings,
SslBundles sslBundles) {
this.connectorBuilder = connectorBuilder;
this.settings = settings;
this.sslBundles = sslBundles;
}
@Override
public Consumer<WebClient.Builder> fromBundle(String bundleName) {
return fromBundle(this.sslBundles.getBundle(bundleName));
}
@Override
public Consumer<WebClient.Builder> fromBundle(SslBundle bundle) {
return (builder) -> {
ClientHttpConnectorSettings settings = this.settings.withSslBundle(bundle);
ClientHttpConnector connector = this.connectorBuilder.build(settings);
builder.clientConnector(connector);
};
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2012-2025 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;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder;
import org.springframework.boot.http.client.reactive.ClientHttpConnectorSettings;
import org.springframework.boot.http.client.reactive.autoconfigure.ClientHttpConnectorAutoConfiguration;
import org.springframework.boot.http.codec.CodecCustomizer;
import org.springframework.boot.http.codec.autoconfigure.CodecsAutoConfiguration;
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.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.core.annotation.Order;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link WebClient}.
* <p>
* This will produce a
* {@link org.springframework.web.reactive.function.client.WebClient.Builder
* WebClient.Builder} bean with the {@code prototype} scope, meaning each injection point
* will receive a newly cloned instance of the builder.
*
* @author Brian Clozel
* @author Phillip Webb
* @since 4.0.0
*/
@AutoConfiguration(after = { ClientHttpConnectorAutoConfiguration.class, CodecsAutoConfiguration.class })
@ConditionalOnClass(WebClient.class)
public class WebClientAutoConfiguration {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@ConditionalOnMissingBean
public WebClient.Builder webClientBuilder(ObjectProvider<WebClientCustomizer> customizerProvider) {
WebClient.Builder builder = WebClient.builder();
customizerProvider.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder;
}
@Bean
@Lazy
@Order(0)
@ConditionalOnBean(ClientHttpConnector.class)
public WebClientCustomizer webClientHttpConnectorCustomizer(ClientHttpConnector clientHttpConnector) {
return (builder) -> builder.clientConnector(clientHttpConnector);
}
@Bean
@ConditionalOnMissingBean(WebClientSsl.class)
@ConditionalOnBean(SslBundles.class)
AutoConfiguredWebClientSsl webClientSsl(ClientHttpConnectorBuilder<?> clientHttpConnectorBuilder,
ClientHttpConnectorSettings clientHttpConnectorSettings, SslBundles sslBundles) {
return new AutoConfiguredWebClientSsl(clientHttpConnectorBuilder, clientHttpConnectorSettings, sslBundles);
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(CodecCustomizer.class)
protected static class WebClientCodecsConfiguration {
@Bean
@ConditionalOnMissingBean
@Order(0)
public WebClientCodecCustomizer exchangeStrategiesCustomizer(ObjectProvider<CodecCustomizer> codecCustomizers) {
return new WebClientCodecCustomizer(codecCustomizers.orderedStream().toList());
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2012-2025 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;
import java.util.List;
import org.springframework.boot.http.codec.CodecCustomizer;
import org.springframework.boot.webclient.WebClientCustomizer;
import org.springframework.web.reactive.function.client.WebClient;
/**
* {@link WebClientCustomizer} that configures codecs for the HTTP client.
*
* @author Brian Clozel
* @since 4.0.0
*/
public class WebClientCodecCustomizer implements WebClientCustomizer {
private final List<CodecCustomizer> codecCustomizers;
public WebClientCodecCustomizer(List<CodecCustomizer> codecCustomizers) {
this.codecCustomizers = codecCustomizers;
}
@Override
public void customize(WebClient.Builder webClientBuilder) {
webClientBuilder
.codecs((codecs) -> this.codecCustomizers.forEach((customizer) -> customizer.customize(codecs)));
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2012-2025 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;
import java.util.function.Consumer;
import org.springframework.boot.ssl.NoSuchSslBundleException;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
/**
* Interface that can be used to {@link WebClient.Builder#apply apply} SSL configuration
* to a {@link org.springframework.web.reactive.function.client.WebClient.Builder
* WebClient.Builder}.
* <p>
* Typically used as follows: <pre class="code">
* &#064;Bean
* public MyBean myBean(WebClient.Builder webClientBuilder, WebClientSsl ssl) {
* WebClient webClient = webClientBuilder.apply(ssl.fromBundle("mybundle")).build();
* return new MyBean(webClient);
* }
* </pre> NOTE: Apply SSL configuration will replace any previously
* {@link WebClient.Builder#clientConnector configured} {@link ClientHttpConnector}.
*
* @author Phillip Webb
* @since 4.0.0
*/
public interface WebClientSsl {
/**
* Return a {@link Consumer} that will apply SSL configuration for the named
* {@link SslBundle} to a
* {@link org.springframework.web.reactive.function.client.WebClient.Builder
* WebClient.Builder}.
* @param bundleName the name of the SSL bundle to apply
* @return a {@link Consumer} to apply the configuration
* @throws NoSuchSslBundleException if a bundle with the provided name does not exist
*/
Consumer<WebClient.Builder> fromBundle(String bundleName) throws NoSuchSslBundleException;
/**
* Return a {@link Consumer} that will apply SSL configuration for the
* {@link SslBundle} to a
* {@link org.springframework.web.reactive.function.client.WebClient.Builder
* WebClient.Builder}.
* @param bundle the SSL bundle to apply
* @return a {@link Consumer} to apply the configuration
*/
Consumer<WebClient.Builder> fromBundle(SslBundle bundle);
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
*/
/**
* Auto-configuration for Spring Framework's functional web client.
*/
package org.springframework.boot.webclient.autoconfigure;

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2019 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.
*/
/**
* Spring WebFlux WebClient support abstractions.
*/
package org.springframework.boot.webclient;

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2012-2025 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.service.autoconfigure;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.boot.http.client.reactive.autoconfigure.AbstractClientHttpConnectorProperties;
/**
* {@link AbstractClientHttpConnectorProperties} for reactive HTTP Service clients.
*
* @author Olga Maciaszek-Sharma
* @author Rossen Stoyanchev
* @author Phillip Webb
* @since 4.0.0
*/
public abstract class AbstractHttpReactiveClientServiceProperties extends AbstractClientHttpConnectorProperties {
/**
* Base url to set in the underlying HTTP client group. By default, set to
* {@code null}.
*/
private String baseUrl;
/**
* Default request headers for interface client group. By default, set to empty
* {@link Map}.
*/
private Map<String, List<String>> defaultHeader = new LinkedHashMap<>();
public String getBaseUrl() {
return this.baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public Map<String, List<String>> getDefaultHeader() {
return this.defaultHeader;
}
public void setDefaultHeader(Map<String, List<String>> defaultHeaders) {
this.defaultHeader = defaultHeaders;
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2012-2025 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.service.autoconfigure;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Properties for Reactive HTTP Service clients.
*
* @author Olga Maciaszek-Sharma
* @author Rossen Stoyanchev
* @author Phillip Webb
* @since 4.0.0
*/
@ConfigurationProperties("spring.http.reactiveclient.service")
public class ReactiveHttpClientServiceProperties extends AbstractHttpReactiveClientServiceProperties {
/**
* Group settings.
*/
private Map<String, Group> group = new LinkedHashMap<>();
public Map<String, Group> getGroup() {
return this.group;
}
public void setGroup(Map<String, Group> group) {
this.group = group;
}
/**
* Properties for a single HTTP Service client group.
*/
public static class Group extends AbstractHttpReactiveClientServiceProperties {
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2012-2025 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.service.autoconfigure;
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.reactive.ClientHttpConnectorBuilder;
import org.springframework.boot.http.client.reactive.ClientHttpConnectorSettings;
import org.springframework.boot.http.client.reactive.autoconfigure.ClientHttpConnectorAutoConfiguration;
import org.springframework.boot.http.client.reactive.autoconfigure.HttpReactiveClientProperties;
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.reactive.function.client.support.WebClientAdapter;
import org.springframework.web.service.registry.HttpServiceProxyRegistry;
import org.springframework.web.service.registry.ImportHttpServices;
/**
* AutoConfiguration for Spring reactive HTTP Service Clients.
* <p>
* This will result in the creation of reactive HTTP Service client beans defined by
* {@link ImportHttpServices @ImportHttpServices} annotations.
*
* @author Olga Maciaszek-Sharma
* @author Rossen Stoyanchev
* @author Phillip Webb
* @since 4.0.0
*/
@AutoConfiguration(after = { ClientHttpConnectorAutoConfiguration.class, WebClientAutoConfiguration.class })
@ConditionalOnClass(WebClientAdapter.class)
@ConditionalOnBean(HttpServiceProxyRegistry.class)
@EnableConfigurationProperties(ReactiveHttpClientServiceProperties.class)
public class ReactiveHttpServiceClientAutoConfiguration implements BeanClassLoaderAware {
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) {
return new WebClientPropertiesHttpServiceGroupConfigurer(this.beanClassLoader, sslBundles,
httpReactiveClientProperties, serviceProperties, clientConnectorBuilder, clientConnectorSettings);
}
@Bean
WebClientCustomizerHttpServiceGroupConfigurer webClientCustomizerHttpServiceGroupConfigurer(
ObjectProvider<WebClientCustomizer> customizers) {
return new WebClientCustomizerHttpServiceGroupConfigurer(customizers);
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2012-2025 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.service.autoconfigure;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.webclient.WebClientCustomizer;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientHttpServiceGroupConfigurer;
import org.springframework.web.service.registry.HttpServiceGroup;
/**
* A {@link RestClientHttpServiceGroupConfigurer} to apply auto-configured
* {@link WebClientCustomizer} beans to the group's {@link RestClient}.
*
* @author Olga Maciaszek-Sharma
* @author Phillip Webb
*/
class WebClientCustomizerHttpServiceGroupConfigurer implements WebClientHttpServiceGroupConfigurer {
/**
* Allow user defined configurers to apply before / after ours.
*/
private static final int ORDER = 0;
private final ObjectProvider<WebClientCustomizer> customizers;
WebClientCustomizerHttpServiceGroupConfigurer(ObjectProvider<WebClientCustomizer> customizers) {
this.customizers = customizers;
}
@Override
public int getOrder() {
return ORDER;
}
@Override
public void configureGroups(Groups<WebClient.Builder> groups) {
groups.forEachClient(this::configureClient);
}
private void configureClient(HttpServiceGroup group, WebClient.Builder builder) {
this.customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2012-2025 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.service.autoconfigure;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder;
import org.springframework.boot.http.client.reactive.ClientHttpConnectorSettings;
import org.springframework.boot.http.client.reactive.autoconfigure.ClientHttpConnectors;
import org.springframework.boot.http.client.reactive.autoconfigure.HttpReactiveClientProperties;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientHttpServiceGroupConfigurer;
import org.springframework.web.service.registry.HttpServiceGroup;
/**
* A {@link RestClientHttpServiceGroupConfigurer} that configures the group and its
* underlying {@link RestClient} using {@link HttpReactiveClientProperties}.
*
* @author Olga Maciaszek-Sharma
* @author Phillip Webb
*/
class WebClientPropertiesHttpServiceGroupConfigurer implements WebClientHttpServiceGroupConfigurer {
private final ClassLoader classLoader;
private final ObjectProvider<SslBundles> sslBundles;
private final HttpReactiveClientProperties clientProperties;
private final ReactiveHttpClientServiceProperties serviceProperties;
private final ObjectProvider<ClientHttpConnectorBuilder<?>> clientConnectorBuilder;
private final ObjectProvider<ClientHttpConnectorSettings> clientConnectorSettings;
WebClientPropertiesHttpServiceGroupConfigurer(ClassLoader classLoader, ObjectProvider<SslBundles> sslBundles,
HttpReactiveClientProperties clientProperties, ReactiveHttpClientServiceProperties serviceProperties,
ObjectProvider<ClientHttpConnectorBuilder<?>> clientConnectorBuilder,
ObjectProvider<ClientHttpConnectorSettings> clientConnectorSettings) {
this.classLoader = classLoader;
this.sslBundles = sslBundles;
this.clientProperties = clientProperties;
this.serviceProperties = serviceProperties;
this.clientConnectorBuilder = clientConnectorBuilder;
this.clientConnectorSettings = clientConnectorSettings;
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
public void configureGroups(Groups<WebClient.Builder> groups) {
groups.forEachClient(this::configureClient);
}
private void configureClient(HttpServiceGroup group, WebClient.Builder builder) {
ReactiveHttpClientServiceProperties.Group groupProperties = this.serviceProperties.getGroup().get(group.name());
builder.clientConnector(getClientConnector(groupProperties));
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this.serviceProperties::getBaseUrl).whenHasText().to(builder::baseUrl);
map.from(this.serviceProperties::getDefaultHeader).as(this::putAllHeaders).to(builder::defaultHeaders);
if (groupProperties != null) {
map.from(groupProperties::getBaseUrl).whenHasText().to(builder::baseUrl);
map.from(groupProperties::getDefaultHeader).as(this::putAllHeaders).to(builder::defaultHeaders);
}
}
private Consumer<HttpHeaders> putAllHeaders(Map<String, List<String>> defaultHeaders) {
return (httpHeaders) -> httpHeaders.putAll(defaultHeaders);
}
private ClientHttpConnector getClientConnector(ReactiveHttpClientServiceProperties.Group groupProperties) {
ClientHttpConnectors connectors = new ClientHttpConnectors(this.sslBundles, groupProperties,
this.serviceProperties, this.clientProperties);
ClientHttpConnectorBuilder<?> builder = this.clientConnectorBuilder
.getIfAvailable(() -> connectors.builder(this.classLoader));
ClientHttpConnectorSettings settings = this.clientConnectorSettings.getIfAvailable(connectors::settings);
return builder.build(settings);
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
*/
/**
* Auto-Configuration for Spring's Reactive HTTP Service Interface Clients.
*/
package org.springframework.boot.webclient.service.autoconfigure;

View File

@@ -0,0 +1,2 @@
org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration
org.springframework.boot.webclient.service.autoconfigure.ReactiveHttpServiceClientAutoConfiguration

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2012-2025 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.actuate.observation;
import io.micrometer.observation.tck.TestObservationRegistry;
import org.junit.jupiter.api.Test;
import org.springframework.web.reactive.function.client.ClientRequestObservationConvention;
import org.springframework.web.reactive.function.client.DefaultClientRequestObservationConvention;
import org.springframework.web.reactive.function.client.WebClient;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ObservationWebClientCustomizer}
*
* @author Brian Clozel
*/
class ObservationWebClientCustomizerTests {
private static final String TEST_METRIC_NAME = "http.test.metric.name";
private final TestObservationRegistry observationRegistry = TestObservationRegistry.create();
private final ClientRequestObservationConvention observationConvention = new DefaultClientRequestObservationConvention(
TEST_METRIC_NAME);
private final ObservationWebClientCustomizer customizer = new ObservationWebClientCustomizer(
this.observationRegistry, this.observationConvention);
private final WebClient.Builder clientBuilder = WebClient.builder();
@Test
void shouldCustomizeObservationConfiguration() {
this.customizer.customize(this.clientBuilder);
assertThat(this.clientBuilder).hasFieldOrPropertyWithValue("observationRegistry", this.observationRegistry);
assertThat(this.clientBuilder).extracting("observationConvention")
.isInstanceOf(DefaultClientRequestObservationConvention.class)
.hasFieldOrPropertyWithValue("name", TEST_METRIC_NAME);
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2012-2025 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;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.http.codec.CodecCustomizer;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.webclient.WebClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.CodecConfigurer;
import org.springframework.web.reactive.function.client.WebClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link WebClientAutoConfiguration}
*
* @author Brian Clozel
*/
class WebClientAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
org.springframework.boot.http.client.reactive.autoconfigure.ClientHttpConnectorAutoConfiguration.class,
WebClientAutoConfiguration.class, SslAutoConfiguration.class));
@Test
void shouldCreateBuilder() {
this.contextRunner.run((context) -> {
WebClient.Builder builder = context.getBean(WebClient.Builder.class);
WebClient webClient = builder.build();
assertThat(webClient).isNotNull();
});
}
@Test
void shouldCustomizeClientCodecs() {
this.contextRunner.withUserConfiguration(CodecConfiguration.class).run((context) -> {
WebClient.Builder builder = context.getBean(WebClient.Builder.class);
CodecCustomizer codecCustomizer = context.getBean(CodecCustomizer.class);
WebClientCodecCustomizer clientCustomizer = context.getBean(WebClientCodecCustomizer.class);
builder.build();
assertThat(clientCustomizer).isNotNull();
then(codecCustomizer).should().customize(any(CodecConfigurer.class));
});
}
@Test
void webClientShouldApplyCustomizers() {
this.contextRunner.withUserConfiguration(WebClientCustomizerConfig.class).run((context) -> {
WebClient.Builder builder = context.getBean(WebClient.Builder.class);
WebClientCustomizer customizer = context.getBean("webClientCustomizer", WebClientCustomizer.class);
builder.build();
then(customizer).should().customize(any(WebClient.Builder.class));
});
}
@Test
void shouldGetPrototypeScopedBean() {
this.contextRunner.withUserConfiguration(WebClientCustomizerConfig.class).run((context) -> {
WebClient.Builder firstBuilder = context.getBean(WebClient.Builder.class);
WebClient.Builder secondBuilder = context.getBean(WebClient.Builder.class);
assertThat(firstBuilder).isNotEqualTo(secondBuilder);
});
}
@Test
void shouldNotCreateClientBuilderIfAlreadyPresent() {
this.contextRunner.withUserConfiguration(WebClientCustomizerConfig.class, CustomWebClientBuilderConfig.class)
.run((context) -> {
WebClient.Builder builder = context.getBean(WebClient.Builder.class);
assertThat(builder).isInstanceOf(MyWebClientBuilder.class);
});
}
@Test
void shouldCreateWebClientSsl() {
this.contextRunner.run((context) -> {
WebClientSsl webClientSsl = context.getBean(WebClientSsl.class);
assertThat(webClientSsl).isInstanceOf(AutoConfiguredWebClientSsl.class);
});
}
@Configuration(proxyBeanMethods = false)
static class CodecConfiguration {
@Bean
CodecCustomizer myCodecCustomizer() {
return mock(CodecCustomizer.class);
}
}
@Configuration(proxyBeanMethods = false)
static class WebClientCustomizerConfig {
@Bean
WebClientCustomizer webClientCustomizer() {
return mock(WebClientCustomizer.class);
}
}
@Configuration(proxyBeanMethods = false)
static class CustomWebClientBuilderConfig {
@Bean
MyWebClientBuilder myWebClientBuilder() {
return mock(MyWebClientBuilder.class);
}
}
interface MyWebClientBuilder extends WebClient.Builder {
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2012-2025 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.service.autoconfigure;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.http.client.HttpRedirects;
import org.springframework.boot.http.client.reactive.autoconfigure.AbstractClientHttpConnectorProperties.Connector;
import org.springframework.boot.webclient.service.autoconfigure.ReactiveHttpClientServiceProperties.Group;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ReactiveHttpClientServiceProperties}.
*
* @author Phillip Webb
*/
class ReactiveHttpClientServicePropertiesTests {
@Test
void bindProperties() {
MockEnvironment environment = new MockEnvironment();
environment.setProperty("spring.http.reactiveclient.service.base-url", "https://example.com");
environment.setProperty("spring.http.reactiveclient.service.default-header.secure", "very,somewhat");
environment.setProperty("spring.http.reactiveclient.service.default-header.test", "true");
environment.setProperty("spring.http.reactiveclient.service.connector", "jetty");
environment.setProperty("spring.http.reactiveclient.service.redirects", "dont-follow");
environment.setProperty("spring.http.reactiveclient.service.connect-timeout", "1s");
environment.setProperty("spring.http.reactiveclient.service.read-timeout", "2s");
environment.setProperty("spring.http.reactiveclient.service.ssl.bundle", "usual");
environment.setProperty("spring.http.reactiveclient.service.group.olga.base-url", "https://example.com/olga");
environment.setProperty("spring.http.reactiveclient.service.group.olga.default-header.secure", "nope");
environment.setProperty("spring.http.reactiveclient.service.group.olga.connector", "reactor");
environment.setProperty("spring.http.reactiveclient.service.group.olga.redirects", "follow");
environment.setProperty("spring.http.reactiveclient.service.group.olga.connect-timeout", "10s");
environment.setProperty("spring.http.reactiveclient.service.group.olga.read-timeout", "20s");
environment.setProperty("spring.http.reactiveclient.service.group.olga.ssl.bundle", "unusual");
environment.setProperty("spring.http.reactiveclient.service.group.rossen.base-url",
"https://example.com/rossen");
environment.setProperty("spring.http.reactiveclient.service.group.phil.base-url", "https://example.com/phil");
try (AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext()) {
applicationContext.setEnvironment(environment);
applicationContext.register(PropertiesConfiguration.class);
applicationContext.refresh();
ReactiveHttpClientServiceProperties properties = applicationContext
.getBean(ReactiveHttpClientServiceProperties.class);
assertThat(properties.getBaseUrl()).isEqualTo("https://example.com");
assertThat(properties.getDefaultHeader()).containsOnly(Map.entry("secure", List.of("very", "somewhat")),
Map.entry("test", List.of("true")));
assertThat(properties.getConnector()).isEqualTo(Connector.JETTY);
assertThat(properties.getRedirects()).isEqualTo(HttpRedirects.DONT_FOLLOW);
assertThat(properties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(1));
assertThat(properties.getReadTimeout()).isEqualTo(Duration.ofSeconds(2));
assertThat(properties.getSsl().getBundle()).isEqualTo("usual");
assertThat(properties.getGroup()).containsOnlyKeys("olga", "rossen", "phil");
assertThat(properties.getGroup().get("olga").getBaseUrl()).isEqualTo("https://example.com/olga");
assertThat(properties.getGroup().get("rossen").getBaseUrl()).isEqualTo("https://example.com/rossen");
assertThat(properties.getGroup().get("phil").getBaseUrl()).isEqualTo("https://example.com/phil");
Group groupProperties = properties.getGroup().get("olga");
assertThat(groupProperties.getDefaultHeader()).containsOnly(Map.entry("secure", List.of("nope")));
assertThat(groupProperties.getConnector()).isEqualTo(Connector.REACTOR);
assertThat(groupProperties.getRedirects()).isEqualTo(HttpRedirects.FOLLOW);
assertThat(groupProperties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(10));
assertThat(groupProperties.getReadTimeout()).isEqualTo(Duration.ofSeconds(20));
assertThat(groupProperties.getSsl().getBundle()).isEqualTo("unusual");
}
}
@Configuration
@EnableConfigurationProperties(ReactiveHttpClientServiceProperties.class)
static class PropertiesConfiguration {
}
}

View File

@@ -0,0 +1,223 @@
/*
* Copyright 2012-2025 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.service.autoconfigure;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Redirect;
import java.util.List;
import java.util.Map;
import org.assertj.core.extractor.Extractors;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Advisor;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.http.client.HttpRedirects;
import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder;
import org.springframework.boot.http.client.reactive.ClientHttpConnectorSettings;
import org.springframework.boot.http.client.reactive.autoconfigure.ClientHttpConnectorAutoConfiguration;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.webclient.WebClientCustomizer;
import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientHttpServiceGroupConfigurer;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.registry.HttpServiceGroup.ClientType;
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;
/**
* Tests for {@link ReactiveHttpServiceClientAutoConfiguration},
* {@link WebClientPropertiesHttpServiceGroupConfigurer} and
* {@link WebClientCustomizerHttpServiceGroupConfigurer}.
*
* @author Phillip Webb
*/
class ReactiveHttpServiceClientAutoConfigurationTests {
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ReactiveHttpServiceClientAutoConfiguration.class,
ClientHttpConnectorAutoConfiguration.class, WebClientAutoConfiguration.class));
@Test
void configuresClientFromProperties() {
this.contextRunner
.withPropertyValues("spring.http.reactiveclient.service.base-url=https://example.com",
"spring.http.reactiveclient.service.default-header.test=true",
"spring.http.reactiveclient.service.group.one.base-url=https://example.com/one",
"spring.http.reactiveclient.service.group.two.default-header.two=iam2")
.withUserConfiguration(HttpClientConfiguration.class)
.run((context) -> {
HttpServiceProxyRegistry serviceProxyRegistry = context.getBean(HttpServiceProxyRegistry.class);
assertThat(serviceProxyRegistry.getGroupNames()).containsOnly("one", "two");
TestClientOne clientOne = context.getBean(TestClientOne.class);
WebClient webClientOne = getWebClient(clientOne);
assertThat(getUriComponentsBuilder(webClientOne).toUriString()).isEqualTo("https://example.com/one");
assertThat(getHttpHeaders(webClientOne).headerSet())
.containsExactlyInAnyOrder(Map.entry("test", List.of("true")));
TestClientTwo clientTwo = context.getBean(TestClientTwo.class);
WebClient webClientTwo = getWebClient(clientTwo);
assertThat(getUriComponentsBuilder(webClientTwo).toUriString()).isEqualTo("https://example.com");
assertThat(getHttpHeaders(webClientTwo).headerSet())
.containsExactlyInAnyOrder(Map.entry("test", List.of("true")), Map.entry("two", List.of("iam2")));
});
}
@Test
void whenHasUserDefinedHttpConnectorBuilder() {
this.contextRunner.withPropertyValues("spring.http.reactiveclient.service.base-url=https://example.com")
.withUserConfiguration(HttpClientConfiguration.class, HttpConnectorBuilderConfiguration.class)
.run((context) -> {
TestClientOne clientOne = context.getBean(TestClientOne.class);
assertThat(getJdkHttpClient(clientOne).followRedirects()).isEqualTo(Redirect.NEVER);
});
}
@Test
void whenHasUserDefinedRequestFactorySettings() {
this.contextRunner
.withPropertyValues("spring.http.reactiveclient.service.base-url=https://example.com",
"spring.http.reactiveclient.connector=jdk")
.withUserConfiguration(HttpClientConfiguration.class, HttpConnectorSettingsConfiguration.class)
.run((context) -> {
TestClientOne clientOne = context.getBean(TestClientOne.class);
assertThat(getJdkHttpClient(clientOne).followRedirects()).isEqualTo(Redirect.NEVER);
});
}
@Test
void whenHasUserDefinedWebClientCustomizer() {
this.contextRunner.withPropertyValues("spring.http.reactiveclient.service.base-url=https://example.com")
.withUserConfiguration(HttpClientConfiguration.class, WebClientCustomizerConfiguration.class)
.run((context) -> {
TestClientOne clientOne = context.getBean(TestClientOne.class);
WebClient webClientOne = getWebClient(clientOne);
assertThat(getHttpHeaders(webClientOne).headerSet())
.containsExactlyInAnyOrder(Map.entry("customized", List.of("true")));
});
}
@Test
void whenHasUserDefinedHttpServiceGroupConfigurer() {
this.contextRunner.withPropertyValues("spring.http.reactiveclient.service.base-url=https://example.com")
.withUserConfiguration(HttpClientConfiguration.class, HttpServiceGroupConfigurerConfiguration.class)
.run((context) -> {
TestClientOne clientOne = context.getBean(TestClientOne.class);
WebClient webClientOne = getWebClient(clientOne);
assertThat(getHttpHeaders(webClientOne).headerSet())
.containsExactlyInAnyOrder(Map.entry("customizedgroup", List.of("true")));
});
}
@Test
void whenHasNoHttpServiceProxyRegistryBean() {
this.contextRunner.withPropertyValues("spring.http.client.reactiveclient.base-url=https://example.com")
.run((context) -> assertThat(context).doesNotHaveBean(HttpServiceProxyRegistry.class));
}
private HttpClient getJdkHttpClient(Object proxy) {
return (HttpClient) Extractors.byName("builder.connector.httpClient").apply(getWebClient(proxy));
}
private HttpHeaders getHttpHeaders(WebClient webClient) {
return (HttpHeaders) Extractors.byName("defaultHeaders").apply(webClient);
}
private UriComponentsBuilder getUriComponentsBuilder(WebClient webClient) {
return (UriComponentsBuilder) Extractors.byName("uriBuilderFactory.baseUri").apply(webClient);
}
private WebClient getWebClient(Object proxy) {
InvocationHandler handler = Proxy.getInvocationHandler(proxy);
Advisor[] advisors = (Advisor[]) Extractors.byName("advised.advisors").apply(handler);
Map<?, ?> serviceMethods = (Map<?, ?>) Extractors.byName("advice.httpServiceMethods").apply(advisors[0]);
Object serviceMethod = serviceMethods.values().iterator().next();
return (WebClient) Extractors.byName("responseFunction.responseFunction.arg$1.webClient").apply(serviceMethod);
}
@Configuration(proxyBeanMethods = false)
@ImportHttpServices(group = "one", types = TestClientOne.class, clientType = ClientType.WEB_CLIENT)
@ImportHttpServices(group = "two", types = TestClientTwo.class, clientType = ClientType.WEB_CLIENT)
static class HttpClientConfiguration {
}
@Configuration(proxyBeanMethods = false)
static class HttpConnectorBuilderConfiguration {
@Bean
ClientHttpConnectorBuilder<?> httpConnectorBuilder() {
return ClientHttpConnectorBuilder.jdk()
.withHttpClientCustomizer((httpClient) -> httpClient.followRedirects(Redirect.NEVER));
}
}
@Configuration(proxyBeanMethods = false)
static class HttpConnectorSettingsConfiguration {
@Bean
ClientHttpConnectorSettings httpConnectorSettings() {
return ClientHttpConnectorSettings.defaults().withRedirects(HttpRedirects.DONT_FOLLOW);
}
}
@Configuration(proxyBeanMethods = false)
static class WebClientCustomizerConfiguration {
@Bean
WebClientCustomizer webClientCustomizer() {
return (builder) -> builder.defaultHeader("customized", "true");
}
}
@Configuration(proxyBeanMethods = false)
static class HttpServiceGroupConfigurerConfiguration {
@Bean
WebClientHttpServiceGroupConfigurer restClientHttpServiceGroupConfigurer() {
return (groups) -> groups.filterByName("one")
.forEachClient((group, builder) -> builder.defaultHeader("customizedgroup", "true"));
}
}
interface TestClientOne {
@GetExchange("/hello")
String hello();
}
interface TestClientTwo {
@GetExchange("/there")
String there();
}
}