Support declarative HTTP Service clients
Add auto-configuration support for Spring Framework's HTTP Service clients, supporting both `RestClient` and `WebClient` client types. Configuration properties are provided under `spring.http.client.service` and `spring.http.reactiveclient.service` names. Group specific properties are available under `....service.group.<name>.`. Auto-configuration support includes automatic registration of user defined `HttpServiceGroupConfigurer` beans. `RestClientCustomizer` and `WebClientCustomizer` beans are also supported. Closes gh-31337 Co-authored-by: Rossen Stoyanchev <rossen.stoyanchev@broadcom.com> Co-authored-by: Phillip Webb <phil.webb@broadcom.com>
This commit is contained in:
committed by
Phillip Webb
parent
f1f54382ed
commit
8f6a9b9440
@@ -21,7 +21,7 @@ import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.boot.autoconfigure.http.client.AbstractHttpClientProperties.Ssl;
|
||||
import org.springframework.boot.autoconfigure.http.client.AbstractHttpRequestFactoryProperties.Factory;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
@@ -37,25 +37,26 @@ import org.springframework.util.StringUtils;
|
||||
* {@link ClientHttpRequestFactorySettings}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
class ClientHttpRequestFactories {
|
||||
public final class ClientHttpRequestFactories {
|
||||
|
||||
private final ObjectProvider<SslBundles> sslBundles;
|
||||
private final ObjectFactory<SslBundles> sslBundles;
|
||||
|
||||
private final AbstractHttpRequestFactoryProperties[] orderedProperties;
|
||||
|
||||
ClientHttpRequestFactories(ObjectProvider<SslBundles> sslBundles,
|
||||
public ClientHttpRequestFactories(ObjectFactory<SslBundles> sslBundles,
|
||||
AbstractHttpRequestFactoryProperties... orderedProperties) {
|
||||
this.sslBundles = sslBundles;
|
||||
this.orderedProperties = orderedProperties;
|
||||
}
|
||||
|
||||
ClientHttpRequestFactoryBuilder<?> builder(ClassLoader classLoader) {
|
||||
public ClientHttpRequestFactoryBuilder<?> builder(ClassLoader classLoader) {
|
||||
Factory factory = getProperty(AbstractHttpRequestFactoryProperties::getFactory);
|
||||
return (factory != null) ? factory.builder() : ClientHttpRequestFactoryBuilder.detect(classLoader);
|
||||
}
|
||||
|
||||
ClientHttpRequestFactorySettings settings() {
|
||||
public ClientHttpRequestFactorySettings settings() {
|
||||
HttpRedirects redirects = getProperty(AbstractHttpRequestFactoryProperties::getRedirects);
|
||||
Duration connectTimeout = getProperty(AbstractHttpRequestFactoryProperties::getConnectTimeout);
|
||||
Duration readTimeout = getProperty(AbstractHttpRequestFactoryProperties::getReadTimeout);
|
||||
|
||||
@@ -21,7 +21,7 @@ import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.boot.autoconfigure.http.client.AbstractHttpClientProperties.Ssl;
|
||||
import org.springframework.boot.autoconfigure.http.client.reactive.AbstractClientHttpConnectorProperties.Connector;
|
||||
import org.springframework.boot.http.client.HttpRedirects;
|
||||
@@ -36,25 +36,26 @@ import org.springframework.util.StringUtils;
|
||||
* {@link ClientHttpConnectorSettings}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
class ClientHttpConnectors {
|
||||
public final class ClientHttpConnectors {
|
||||
|
||||
private final ObjectProvider<SslBundles> sslBundles;
|
||||
private final ObjectFactory<SslBundles> sslBundles;
|
||||
|
||||
private final AbstractClientHttpConnectorProperties[] orderedProperties;
|
||||
|
||||
ClientHttpConnectors(ObjectProvider<SslBundles> sslBundles,
|
||||
public ClientHttpConnectors(ObjectFactory<SslBundles> sslBundles,
|
||||
AbstractClientHttpConnectorProperties... orderedProperties) {
|
||||
this.sslBundles = sslBundles;
|
||||
this.orderedProperties = orderedProperties;
|
||||
}
|
||||
|
||||
ClientHttpConnectorBuilder<?> builder(ClassLoader classLoader) {
|
||||
public ClientHttpConnectorBuilder<?> builder(ClassLoader classLoader) {
|
||||
Connector connector = getProperty(AbstractClientHttpConnectorProperties::getConnector);
|
||||
return (connector != null) ? connector.builder() : ClientHttpConnectorBuilder.detect(classLoader);
|
||||
}
|
||||
|
||||
ClientHttpConnectorSettings settings() {
|
||||
public ClientHttpConnectorSettings settings() {
|
||||
HttpRedirects redirects = getProperty(AbstractClientHttpConnectorProperties::getRedirects);
|
||||
Duration connectTimeout = getProperty(AbstractClientHttpConnectorProperties::getConnectTimeout);
|
||||
Duration readTimeout = getProperty(AbstractClientHttpConnectorProperties::getReadTimeout);
|
||||
|
||||
@@ -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.autoconfigure.http.client.reactive.service;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.autoconfigure.http.client.reactive.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.autoconfigure.http.client.reactive.service;
|
||||
|
||||
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.autoconfigure.http.client.reactive.service;
|
||||
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.http.client.reactive.ClientHttpConnectorAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.http.client.reactive.HttpReactiveClientProperties;
|
||||
import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
|
||||
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.ssl.SslBundles;
|
||||
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
|
||||
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.autoconfigure.http.client.reactive.service;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.web.client.RestClientCustomizer;
|
||||
import org.springframework.boot.web.reactive.function.client.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;
|
||||
|
||||
/**
|
||||
* A {@link RestClientHttpServiceGroupConfigurer} to apply auto-configured
|
||||
* {@link RestClientCustomizer} 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.configureClient(this::configureClient);
|
||||
}
|
||||
|
||||
private void configureClient(WebClient.Builder builder) {
|
||||
this.customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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.autoconfigure.http.client.reactive.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.http.client.HttpClientProperties;
|
||||
import org.springframework.boot.autoconfigure.http.client.reactive.ClientHttpConnectors;
|
||||
import org.springframework.boot.autoconfigure.http.client.reactive.HttpReactiveClientProperties;
|
||||
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.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 HttpClientProperties}.
|
||||
*
|
||||
* @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.configureClient(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.autoconfigure.http.client.reactive.service;
|
||||
@@ -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.autoconfigure.http.client.service;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.autoconfigure.http.client.AbstractHttpRequestFactoryProperties;
|
||||
|
||||
/**
|
||||
* {@link AbstractHttpRequestFactoryProperties} for HTTP Service clients.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public abstract class AbstractHttpClientServiceProperties extends AbstractHttpRequestFactoryProperties {
|
||||
|
||||
/**
|
||||
* 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.autoconfigure.http.client.service;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Properties for HTTP Service clients.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@ConfigurationProperties("spring.http.client.service")
|
||||
public class HttpClientServiceProperties extends AbstractHttpClientServiceProperties {
|
||||
|
||||
/**
|
||||
* 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 AbstractHttpClientServiceProperties {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.autoconfigure.http.client.service;
|
||||
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.http.client.HttpClientAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.http.client.HttpClientProperties;
|
||||
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.web.client.RestClientCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.client.support.RestClientAdapter;
|
||||
import org.springframework.web.service.registry.HttpServiceProxyRegistry;
|
||||
import org.springframework.web.service.registry.ImportHttpServices;
|
||||
|
||||
/**
|
||||
* AutoConfiguration for Spring HTTP Service clients.
|
||||
* <p>
|
||||
* This will result in the creation of blocking 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 = { HttpClientAutoConfiguration.class, RestClientAutoConfiguration.class })
|
||||
@ConditionalOnClass(RestClientAdapter.class)
|
||||
@ConditionalOnBean(HttpServiceProxyRegistry.class)
|
||||
@EnableConfigurationProperties(HttpClientServiceProperties.class)
|
||||
public class HttpServiceClientAutoConfiguration implements BeanClassLoaderAware {
|
||||
|
||||
private ClassLoader beanClassLoader;
|
||||
|
||||
HttpServiceClientAutoConfiguration() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanClassLoader(ClassLoader classLoader) {
|
||||
this.beanClassLoader = classLoader;
|
||||
}
|
||||
|
||||
@Bean
|
||||
RestClientPropertiesHttpServiceGroupConfigurer restClientPropertiesHttpServiceGroupConfigurer(
|
||||
ObjectProvider<SslBundles> sslBundles, HttpClientProperties httpClientProperties,
|
||||
HttpClientServiceProperties serviceProperties,
|
||||
ObjectProvider<ClientHttpRequestFactoryBuilder<?>> clientFactoryBuilder,
|
||||
ObjectProvider<ClientHttpRequestFactorySettings> clientHttpRequestFactorySettings) {
|
||||
return new RestClientPropertiesHttpServiceGroupConfigurer(this.beanClassLoader, sslBundles,
|
||||
httpClientProperties, serviceProperties, clientFactoryBuilder, clientHttpRequestFactorySettings);
|
||||
}
|
||||
|
||||
@Bean
|
||||
RestClientCustomizerHttpServiceGroupConfigurer restClientCustomizerHttpServiceGroupConfigurer(
|
||||
ObjectProvider<RestClientCustomizer> customizers) {
|
||||
return new RestClientCustomizerHttpServiceGroupConfigurer(customizers);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.autoconfigure.http.client.service;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.web.client.RestClientCustomizer;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
|
||||
|
||||
/**
|
||||
* A {@link RestClientHttpServiceGroupConfigurer} to apply auto-configured
|
||||
* {@link RestClientCustomizer} beans to the group's {@link RestClient}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class RestClientCustomizerHttpServiceGroupConfigurer implements RestClientHttpServiceGroupConfigurer {
|
||||
|
||||
/**
|
||||
* Allow user defined configurers to apply before / after ours.
|
||||
*/
|
||||
private static final int ORDER = 0;
|
||||
|
||||
private final ObjectProvider<RestClientCustomizer> customizers;
|
||||
|
||||
RestClientCustomizerHttpServiceGroupConfigurer(ObjectProvider<RestClientCustomizer> customizers) {
|
||||
this.customizers = customizers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return ORDER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureGroups(Groups<RestClient.Builder> groups) {
|
||||
groups.configureClient(this::configureClient);
|
||||
}
|
||||
|
||||
private void configureClient(RestClient.Builder builder) {
|
||||
this.customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.autoconfigure.http.client.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.http.client.ClientHttpRequestFactories;
|
||||
import org.springframework.boot.autoconfigure.http.client.HttpClientProperties;
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
|
||||
import org.springframework.web.service.registry.HttpServiceGroup;
|
||||
|
||||
/**
|
||||
* A {@link RestClientHttpServiceGroupConfigurer} that configures the group and its
|
||||
* underlying {@link RestClient} using {@link HttpClientProperties}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class RestClientPropertiesHttpServiceGroupConfigurer implements RestClientHttpServiceGroupConfigurer {
|
||||
|
||||
private final ClassLoader classLoader;
|
||||
|
||||
private final ObjectProvider<SslBundles> sslBundles;
|
||||
|
||||
private final HttpClientProperties clientProperties;
|
||||
|
||||
private final HttpClientServiceProperties serviceProperties;
|
||||
|
||||
private final ObjectProvider<ClientHttpRequestFactoryBuilder<?>> requestFactoryBuilder;
|
||||
|
||||
private final ObjectProvider<ClientHttpRequestFactorySettings> requestFactorySettings;
|
||||
|
||||
RestClientPropertiesHttpServiceGroupConfigurer(ClassLoader classLoader, ObjectProvider<SslBundles> sslBundles,
|
||||
HttpClientProperties clientProperties, HttpClientServiceProperties serviceProperties,
|
||||
ObjectProvider<ClientHttpRequestFactoryBuilder<?>> requestFactoryBuilder,
|
||||
ObjectProvider<ClientHttpRequestFactorySettings> requestFactorySettings) {
|
||||
this.classLoader = classLoader;
|
||||
this.sslBundles = sslBundles;
|
||||
this.clientProperties = clientProperties;
|
||||
this.serviceProperties = serviceProperties;
|
||||
this.requestFactoryBuilder = requestFactoryBuilder;
|
||||
this.requestFactorySettings = requestFactorySettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.HIGHEST_PRECEDENCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureGroups(Groups<RestClient.Builder> groups) {
|
||||
groups.configureClient(this::configureClient);
|
||||
}
|
||||
|
||||
private void configureClient(HttpServiceGroup group, RestClient.Builder builder) {
|
||||
HttpClientServiceProperties.Group groupProperties = this.serviceProperties.getGroup().get(group.name());
|
||||
builder.requestFactory(getRequestFactory(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 ClientHttpRequestFactory getRequestFactory(HttpClientServiceProperties.Group groupProperties) {
|
||||
ClientHttpRequestFactories factories = new ClientHttpRequestFactories(this.sslBundles, groupProperties,
|
||||
this.serviceProperties, this.clientProperties);
|
||||
ClientHttpRequestFactoryBuilder<?> builder = this.requestFactoryBuilder
|
||||
.getIfAvailable(() -> factories.builder(this.classLoader));
|
||||
ClientHttpRequestFactorySettings settings = this.requestFactorySettings.getIfAvailable(factories::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 Blocking HTTP Service Interface Clients.
|
||||
*/
|
||||
package org.springframework.boot.autoconfigure.http.client.service;
|
||||
@@ -65,6 +65,8 @@ org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfigurati
|
||||
org.springframework.boot.autoconfigure.http.client.HttpClientAutoConfiguration
|
||||
org.springframework.boot.autoconfigure.http.client.reactive.ClientHttpConnectorAutoConfiguration
|
||||
org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration
|
||||
org.springframework.boot.autoconfigure.http.client.service.HttpServiceClientAutoConfiguration
|
||||
org.springframework.boot.autoconfigure.http.client.reactive.service.ReactiveHttpServiceClientAutoConfiguration
|
||||
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration
|
||||
org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration
|
||||
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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.autoconfigure.http.client;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.boot.autoconfigure.http.client.AbstractHttpRequestFactoryProperties.Factory;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
|
||||
import org.springframework.boot.http.client.HttpComponentsClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.HttpRedirects;
|
||||
import org.springframework.boot.http.client.JettyClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link ClientHttpRequestFactories}
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ClientHttpRequestFactoriesTests {
|
||||
|
||||
private final DefaultSslBundleRegistry bundleRegistry = new DefaultSslBundleRegistry();
|
||||
|
||||
private ObjectFactory<SslBundles> sslBundles = () -> this.bundleRegistry;
|
||||
|
||||
@Test
|
||||
void builderWhenHasFactoryPropertyReturnsFirst() {
|
||||
TestProperties p1 = new TestProperties();
|
||||
TestProperties p2 = new TestProperties();
|
||||
p2.setFactory(Factory.JETTY);
|
||||
TestProperties p3 = new TestProperties();
|
||||
p3.setFactory(Factory.REACTOR);
|
||||
ClientHttpRequestFactories factories = new ClientHttpRequestFactories(this.sslBundles, p1, p2, p3);
|
||||
assertThat(factories.builder(null)).isInstanceOf(JettyClientHttpRequestFactoryBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWhenHasNoFactoryPropertyReturnsDetected() {
|
||||
TestProperties properties = new TestProperties();
|
||||
ClientHttpRequestFactories factories = new ClientHttpRequestFactories(this.sslBundles, properties);
|
||||
assertThat(factories.builder(null)).isInstanceOf(HttpComponentsClientHttpRequestFactoryBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void settingsWhenHasNoSettingProperties() {
|
||||
TestProperties properties = new TestProperties();
|
||||
ClientHttpRequestFactories factories = new ClientHttpRequestFactories(this.sslBundles, properties);
|
||||
ClientHttpRequestFactorySettings settings = factories.settings();
|
||||
assertThat(settings).isEqualTo(new ClientHttpRequestFactorySettings(null, null, null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void settingsWhenHasMultipleSettingProperties() {
|
||||
this.bundleRegistry.registerBundle("p2", mock(SslBundle.class));
|
||||
this.bundleRegistry.registerBundle("p3", mock(SslBundle.class));
|
||||
TestProperties p1 = new TestProperties();
|
||||
TestProperties p2 = new TestProperties();
|
||||
p2.setRedirects(HttpRedirects.DONT_FOLLOW);
|
||||
p2.setConnectTimeout(Duration.ofSeconds(1));
|
||||
p2.setReadTimeout(Duration.ofSeconds(2));
|
||||
p2.getSsl().setBundle("p2");
|
||||
TestProperties p3 = new TestProperties();
|
||||
p3.setRedirects(HttpRedirects.FOLLOW);
|
||||
p3.setConnectTimeout(Duration.ofSeconds(10));
|
||||
p3.setReadTimeout(Duration.ofSeconds(20));
|
||||
p3.getSsl().setBundle("p3");
|
||||
ClientHttpRequestFactories factories = new ClientHttpRequestFactories(this.sslBundles, p1, p2, p3);
|
||||
ClientHttpRequestFactorySettings settings = factories.settings();
|
||||
assertThat(settings).isEqualTo(new ClientHttpRequestFactorySettings(Redirects.DONT_FOLLOW,
|
||||
Duration.ofSeconds(1), Duration.ofSeconds(2), this.bundleRegistry.getBundle("p2")));
|
||||
}
|
||||
|
||||
static class TestProperties extends AbstractHttpRequestFactoryProperties {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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.autoconfigure.http.client.reactive;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.boot.autoconfigure.http.client.reactive.AbstractClientHttpConnectorProperties.Connector;
|
||||
import org.springframework.boot.http.client.HttpRedirects;
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorSettings;
|
||||
import org.springframework.boot.http.client.reactive.JettyClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.ReactorClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link ClientHttpConnectors}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ClientHttpConnectorsTests {
|
||||
|
||||
private final DefaultSslBundleRegistry bundleRegistry = new DefaultSslBundleRegistry();
|
||||
|
||||
private ObjectFactory<SslBundles> sslBundles = () -> this.bundleRegistry;
|
||||
|
||||
@Test
|
||||
void builderWhenHasConnectorPropertyReturnsFirst() {
|
||||
TestProperties p1 = new TestProperties();
|
||||
TestProperties p2 = new TestProperties();
|
||||
p2.setConnector(Connector.JETTY);
|
||||
TestProperties p3 = new TestProperties();
|
||||
p3.setConnector(Connector.JDK);
|
||||
ClientHttpConnectors connectors = new ClientHttpConnectors(this.sslBundles, p1, p2, p3);
|
||||
assertThat(connectors.builder(null)).isInstanceOf(JettyClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWhenHasNoConnectorPropertyReturnsDetected() {
|
||||
TestProperties properties = new TestProperties();
|
||||
ClientHttpConnectors connectors = new ClientHttpConnectors(this.sslBundles, properties);
|
||||
assertThat(connectors.builder(null)).isInstanceOf(ReactorClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void settingsWhenHasNoSettingProperties() {
|
||||
TestProperties properties = new TestProperties();
|
||||
ClientHttpConnectors connectors = new ClientHttpConnectors(this.sslBundles, properties);
|
||||
ClientHttpConnectorSettings settings = connectors.settings();
|
||||
assertThat(settings).isEqualTo(new ClientHttpConnectorSettings(null, null, null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void settingsWhenHasMultipleSettingProperties() {
|
||||
this.bundleRegistry.registerBundle("p2", mock(SslBundle.class));
|
||||
this.bundleRegistry.registerBundle("p3", mock(SslBundle.class));
|
||||
TestProperties p1 = new TestProperties();
|
||||
TestProperties p2 = new TestProperties();
|
||||
p2.setRedirects(HttpRedirects.DONT_FOLLOW);
|
||||
p2.setConnectTimeout(Duration.ofSeconds(1));
|
||||
p2.setReadTimeout(Duration.ofSeconds(2));
|
||||
p2.getSsl().setBundle("p2");
|
||||
TestProperties p3 = new TestProperties();
|
||||
p3.setRedirects(HttpRedirects.FOLLOW);
|
||||
p3.setConnectTimeout(Duration.ofSeconds(10));
|
||||
p3.setReadTimeout(Duration.ofSeconds(20));
|
||||
p3.getSsl().setBundle("p3");
|
||||
ClientHttpConnectors connectors = new ClientHttpConnectors(this.sslBundles, p1, p2, p3);
|
||||
ClientHttpConnectorSettings settings = connectors.settings();
|
||||
assertThat(settings).isEqualTo(new ClientHttpConnectorSettings(HttpRedirects.DONT_FOLLOW, Duration.ofSeconds(1),
|
||||
Duration.ofSeconds(2), this.bundleRegistry.getBundle("p2")));
|
||||
}
|
||||
|
||||
static class TestProperties extends AbstractClientHttpConnectorProperties {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.autoconfigure.http.client.reactive.service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.http.client.reactive.AbstractClientHttpConnectorProperties.Connector;
|
||||
import org.springframework.boot.autoconfigure.http.client.reactive.service.ReactiveHttpClientServiceProperties.Group;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.http.client.HttpRedirects;
|
||||
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.autoconfigure.http.client.reactive.service;
|
||||
|
||||
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.autoconfigure.http.client.reactive.ClientHttpConnectorAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
|
||||
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.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
|
||||
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")
|
||||
.configureClient((builder) -> builder.defaultHeader("customizedgroup", "true"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface TestClientOne {
|
||||
|
||||
@GetExchange("/hello")
|
||||
String hello();
|
||||
|
||||
}
|
||||
|
||||
interface TestClientTwo {
|
||||
|
||||
@GetExchange("/there")
|
||||
String there();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.autoconfigure.http.client.service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.http.client.AbstractHttpRequestFactoryProperties.Factory;
|
||||
import org.springframework.boot.autoconfigure.http.client.service.HttpClientServiceProperties.Group;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.http.client.HttpRedirects;
|
||||
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 HttpClientServiceProperties}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class HttpClientServicePropertiesTests {
|
||||
|
||||
@Test
|
||||
void bindProperties() {
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
environment.setProperty("spring.http.client.service.base-url", "https://example.com");
|
||||
environment.setProperty("spring.http.client.service.default-header.secure", "very,somewhat");
|
||||
environment.setProperty("spring.http.client.service.default-header.test", "true");
|
||||
environment.setProperty("spring.http.client.service.factory", "jetty");
|
||||
environment.setProperty("spring.http.client.service.redirects", "dont-follow");
|
||||
environment.setProperty("spring.http.client.service.connect-timeout", "1s");
|
||||
environment.setProperty("spring.http.client.service.read-timeout", "2s");
|
||||
environment.setProperty("spring.http.client.service.ssl.bundle", "usual");
|
||||
environment.setProperty("spring.http.client.service.group.olga.base-url", "https://example.com/olga");
|
||||
environment.setProperty("spring.http.client.service.group.olga.default-header.secure", "nope");
|
||||
environment.setProperty("spring.http.client.service.group.olga.factory", "reactor");
|
||||
environment.setProperty("spring.http.client.service.group.olga.redirects", "follow");
|
||||
environment.setProperty("spring.http.client.service.group.olga.connect-timeout", "10s");
|
||||
environment.setProperty("spring.http.client.service.group.olga.read-timeout", "20s");
|
||||
environment.setProperty("spring.http.client.service.group.olga.ssl.bundle", "unusual");
|
||||
environment.setProperty("spring.http.client.service.group.rossen.base-url", "https://example.com/rossen");
|
||||
environment.setProperty("spring.http.client.service.group.phil.base-url", "https://example.com/phil");
|
||||
try (AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext()) {
|
||||
applicationContext.setEnvironment(environment);
|
||||
applicationContext.register(PropertiesConfiguration.class);
|
||||
applicationContext.refresh();
|
||||
HttpClientServiceProperties properties = applicationContext.getBean(HttpClientServiceProperties.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.getFactory()).isEqualTo(Factory.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.getFactory()).isEqualTo(Factory.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(HttpClientServiceProperties.class)
|
||||
static class PropertiesConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* 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.autoconfigure.http.client.service;
|
||||
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpClient.Redirect;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
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.autoconfigure.http.client.HttpClientAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.boot.web.client.RestClientCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.web.client.MockRestServiceServer;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.client.RestClient.Builder;
|
||||
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
|
||||
import org.springframework.web.service.annotation.GetExchange;
|
||||
import org.springframework.web.service.registry.HttpServiceGroup;
|
||||
import org.springframework.web.service.registry.HttpServiceProxyRegistry;
|
||||
import org.springframework.web.service.registry.ImportHttpServices;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpServiceClientAutoConfiguration},
|
||||
* {@link RestClientPropertiesHttpServiceGroupConfigurer} and
|
||||
* {@link RestClientCustomizerHttpServiceGroupConfigurer}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class HttpServiceClientAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(HttpServiceClientAutoConfiguration.class,
|
||||
HttpClientAutoConfiguration.class, RestClientAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void configuresClientFromProperties() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.http.client.service.base-url=https://example.com",
|
||||
"spring.http.client.service.default-header.test=true",
|
||||
"spring.http.client.service.group.one.base-url=https://example.com/one",
|
||||
"spring.http.client.service.group.two.default-header.two=iam2")
|
||||
.withUserConfiguration(HttpClientConfiguration.class, MockRestServiceServerConfiguration.class)
|
||||
.run((context) -> {
|
||||
HttpServiceProxyRegistry serviceProxyRegistry = context.getBean(HttpServiceProxyRegistry.class);
|
||||
assertThat(serviceProxyRegistry.getGroupNames()).containsOnly("one", "two");
|
||||
MockRestServiceServerConfiguration mockServers = context
|
||||
.getBean(MockRestServiceServerConfiguration.class);
|
||||
MockRestServiceServer serverOne = mockServers.getMock("one");
|
||||
serverOne.expect(requestTo("https://example.com/one/hello"))
|
||||
.andExpect(header("test", "true"))
|
||||
.andRespond(withSuccess().body("world!"));
|
||||
TestClientOne clientOne = context.getBean(TestClientOne.class);
|
||||
assertThat(clientOne.hello()).isEqualTo("world!");
|
||||
MockRestServiceServer serverTwo = mockServers.getMock("two");
|
||||
serverTwo.expect((request) -> request.getURI().toString().equals("https://example.com/"))
|
||||
.andExpect(header("test", "true"))
|
||||
.andExpect(header("two", "iam2"))
|
||||
.andRespond(withSuccess().body("boot!"));
|
||||
TestClientTwo clientTwo = context.getBean(TestClientTwo.class);
|
||||
assertThat(clientTwo.there()).isEqualTo("boot!");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedRequestFactoryBuilder() {
|
||||
this.contextRunner.withPropertyValues("spring.http.client.service.base-url=https://example.com")
|
||||
.withUserConfiguration(HttpClientConfiguration.class, RequestFactoryBuilderConfiguration.class)
|
||||
.run((context) -> {
|
||||
TestClientOne clientOne = context.getBean(TestClientOne.class);
|
||||
assertThat(getJdkHttpClient(clientOne).followRedirects()).isEqualTo(Redirect.NEVER);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedRequestFactorySettings() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.http.client.service.base-url=https://example.com",
|
||||
"spring.http.client.factory=jdk")
|
||||
.withUserConfiguration(HttpClientConfiguration.class, RequestFactorySettingsConfiguration.class)
|
||||
.run((context) -> {
|
||||
TestClientOne clientOne = context.getBean(TestClientOne.class);
|
||||
assertThat(getJdkHttpClient(clientOne).followRedirects()).isEqualTo(Redirect.NEVER);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedRestClientCustomizer() {
|
||||
this.contextRunner.withPropertyValues("spring.http.client.service.base-url=https://example.com")
|
||||
.withUserConfiguration(HttpClientConfiguration.class, MockRestServiceServerConfiguration.class,
|
||||
RestClientCustomizerConfiguration.class)
|
||||
.run((context) -> {
|
||||
MockRestServiceServerConfiguration mockServers = context
|
||||
.getBean(MockRestServiceServerConfiguration.class);
|
||||
MockRestServiceServer serverOne = mockServers.getMock("one");
|
||||
serverOne.expect(requestTo("https://example.com/hello"))
|
||||
.andExpect(header("customized", "true"))
|
||||
.andRespond(withSuccess().body("world!"));
|
||||
TestClientOne clientOne = context.getBean(TestClientOne.class);
|
||||
assertThat(clientOne.hello()).isEqualTo("world!");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedHttpServiceGroupConfigurer() {
|
||||
this.contextRunner.withPropertyValues("spring.http.client.service.base-url=https://example.com")
|
||||
.withUserConfiguration(HttpClientConfiguration.class, MockRestServiceServerConfiguration.class,
|
||||
HttpServiceGroupConfigurerConfiguration.class)
|
||||
.run((context) -> {
|
||||
MockRestServiceServerConfiguration mockServers = context
|
||||
.getBean(MockRestServiceServerConfiguration.class);
|
||||
MockRestServiceServer serverOne = mockServers.getMock("one");
|
||||
serverOne.expect(requestTo("https://example.com/hello"))
|
||||
.andExpect(header("customizedgroup", "true"))
|
||||
.andRespond(withSuccess().body("world!"));
|
||||
TestClientOne clientOne = context.getBean(TestClientOne.class);
|
||||
assertThat(clientOne.hello()).isEqualTo("world!");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNoHttpServiceProxyRegistryBean() {
|
||||
this.contextRunner.withPropertyValues("spring.http.client.service.base-url=https://example.com")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(HttpServiceProxyRegistry.class));
|
||||
}
|
||||
|
||||
private HttpClient getJdkHttpClient(Object proxy) {
|
||||
return (HttpClient) Extractors.byName("clientRequestFactory.httpClient").apply(getRestClient(proxy));
|
||||
}
|
||||
|
||||
private RestClient getRestClient(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 (RestClient) Extractors.byName("responseFunction.responseFunction.arg$1.restClient")
|
||||
.apply(serviceMethod);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class MockRestServiceServerConfiguration {
|
||||
|
||||
private Map<String, MockRestServiceServer> mocks = new HashMap<>();
|
||||
|
||||
@Bean
|
||||
RestClientHttpServiceGroupConfigurer mockServerConfigurer() {
|
||||
return (groups) -> groups.configureClient((BiConsumer<HttpServiceGroup, Builder>) this::addMock);
|
||||
}
|
||||
|
||||
private MockRestServiceServer addMock(HttpServiceGroup group, Builder client) {
|
||||
return this.mocks.put(group.name(), MockRestServiceServer.bindTo(client).build());
|
||||
}
|
||||
|
||||
MockRestServiceServer getMock(String name) {
|
||||
return this.mocks.get(name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ImportHttpServices(group = "one", types = TestClientOne.class)
|
||||
@ImportHttpServices(group = "two", types = TestClientTwo.class)
|
||||
static class HttpClientConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class RequestFactoryBuilderConfiguration {
|
||||
|
||||
@Bean
|
||||
ClientHttpRequestFactoryBuilder<?> requestFactoryBuilder() {
|
||||
return ClientHttpRequestFactoryBuilder.jdk()
|
||||
.withHttpClientCustomizer((httpClient) -> httpClient.followRedirects(Redirect.NEVER));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class RequestFactorySettingsConfiguration {
|
||||
|
||||
@Bean
|
||||
ClientHttpRequestFactorySettings requestFactorySettings() {
|
||||
return ClientHttpRequestFactorySettings.defaults().withRedirects(Redirects.DONT_FOLLOW);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class RestClientCustomizerConfiguration {
|
||||
|
||||
@Bean
|
||||
RestClientCustomizer restClientCustomizer() {
|
||||
return (builder) -> builder.defaultHeader("customized", "true");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class HttpServiceGroupConfigurerConfiguration {
|
||||
|
||||
@Bean
|
||||
RestClientHttpServiceGroupConfigurer restClientHttpServiceGroupConfigurer() {
|
||||
return (groups) -> groups.filterByName("one")
|
||||
.configureClient((builder) -> builder.defaultHeader("customizedgroup", "true"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface TestClientOne {
|
||||
|
||||
@GetExchange("/hello")
|
||||
String hello();
|
||||
|
||||
}
|
||||
|
||||
interface TestClientTwo {
|
||||
|
||||
@GetExchange("/there")
|
||||
String there();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user