Rework HTTP client modules
This commit is contained in:
committed by
Phillip Webb
parent
4763ef2463
commit
7a9be5bd4a
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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">
|
||||
* @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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration
|
||||
org.springframework.boot.webclient.service.autoconfigure.ReactiveHttpServiceClientAutoConfiguration
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user