Introduce ClientHttpConnectorBuilder support
Add a new `ClientHttpConnectorBuilder` interface to support the creation of `ClientHttpConnector` instances. The new code is similar to the `ClientHttpRequestFactoryBuilder` interface that was added to Spring Boot 3.4. The `ClientHttpConnectorBuilder` is a functional interface with additional static factory methods for the various supported `ClientHttpConnector` types. Each type has it's own builder which to support client specific customization. The previous auto-configuration has been relocated to the `org.springframework.boot.autoconfigure.http.client.reactive` package and updated to make use of the builder. Closes gh-43079
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.http.client.HttpClientSettings;
|
||||
import org.springframework.boot.http.client.HttpRedirects;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Abstract base class for properties that directly or indirectly make use of a blocking
|
||||
* or reactive HTTP client.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
* @see HttpClientSettings
|
||||
*/
|
||||
public abstract class AbstractHttpClientProperties {
|
||||
|
||||
/**
|
||||
* Handling for HTTP redirects.
|
||||
*/
|
||||
private HttpRedirects redirects = HttpRedirects.FOLLOW_WHEN_POSSIBLE;
|
||||
|
||||
/**
|
||||
* Default connect timeout for a client HTTP request.
|
||||
*/
|
||||
private Duration connectTimeout;
|
||||
|
||||
/**
|
||||
* Default read timeout for a client HTTP request.
|
||||
*/
|
||||
private Duration readTimeout;
|
||||
|
||||
/**
|
||||
* Default SSL configuration for a client HTTP request.
|
||||
*/
|
||||
private final Ssl ssl = new Ssl();
|
||||
|
||||
public HttpRedirects getRedirects() {
|
||||
return this.redirects;
|
||||
}
|
||||
|
||||
public void setRedirects(HttpRedirects redirects) {
|
||||
this.redirects = redirects;
|
||||
}
|
||||
|
||||
public Duration getConnectTimeout() {
|
||||
return this.connectTimeout;
|
||||
}
|
||||
|
||||
public void setConnectTimeout(Duration connectTimeout) {
|
||||
this.connectTimeout = connectTimeout;
|
||||
}
|
||||
|
||||
public Duration getReadTimeout() {
|
||||
return this.readTimeout;
|
||||
}
|
||||
|
||||
public void setReadTimeout(Duration readTimeout) {
|
||||
this.readTimeout = readTimeout;
|
||||
}
|
||||
|
||||
public Ssl getSsl() {
|
||||
return this.ssl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@link HttpClientSettings} based on these properties.
|
||||
* @param sslBundles a {@link SslBundles} provider
|
||||
* @return the {@link HttpClientSettings}
|
||||
*/
|
||||
protected HttpClientSettings httpClientSettings(ObjectProvider<SslBundles> sslBundles) {
|
||||
return new HttpClientSettings(this.redirects, this.connectTimeout, this.readTimeout, sslBundle(sslBundles));
|
||||
}
|
||||
|
||||
private SslBundle sslBundle(ObjectProvider<SslBundles> sslBundles) {
|
||||
String name = getSsl().getBundle();
|
||||
return (StringUtils.hasLength(name)) ? sslBundles.getObject().getBundle(name) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* SSL configuration.
|
||||
*/
|
||||
public static class Ssl {
|
||||
|
||||
/**
|
||||
* SSL bundle to use.
|
||||
*/
|
||||
private String bundle;
|
||||
|
||||
public String getBundle() {
|
||||
return this.bundle;
|
||||
}
|
||||
|
||||
public void setBundle(String bundle) {
|
||||
this.bundle = bundle;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.util.function.Supplier;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
|
||||
/**
|
||||
* Base {@link ConfigurationProperties @ConfigurationProperties} for configuring a
|
||||
* {@link ClientHttpRequestFactory}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
* @see ClientHttpRequestFactorySettings
|
||||
*/
|
||||
public abstract class AbstractHttpRequestFactoryProperties extends AbstractHttpClientProperties {
|
||||
|
||||
/**
|
||||
* Default factory used for a client HTTP request.
|
||||
*/
|
||||
private Factory factory;
|
||||
|
||||
public Factory getFactory() {
|
||||
return this.factory;
|
||||
}
|
||||
|
||||
public void setFactory(Factory factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link ClientHttpRequestFactoryBuilder} based on the properties.
|
||||
* @return a {@link ClientHttpRequestFactoryBuilder}
|
||||
*/
|
||||
protected final ClientHttpRequestFactoryBuilder<?> factoryBuilder() {
|
||||
Factory factory = getFactory();
|
||||
return (factory != null) ? factory.builder() : ClientHttpRequestFactoryBuilder.detect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported factory types.
|
||||
*/
|
||||
public enum Factory {
|
||||
|
||||
/**
|
||||
* Apache HttpComponents HttpClient.
|
||||
*/
|
||||
HTTP_COMPONENTS(ClientHttpRequestFactoryBuilder::httpComponents),
|
||||
|
||||
/**
|
||||
* Jetty's HttpClient.
|
||||
*/
|
||||
JETTY(ClientHttpRequestFactoryBuilder::jetty),
|
||||
|
||||
/**
|
||||
* Reactor-Netty.
|
||||
*/
|
||||
REACTOR(ClientHttpRequestFactoryBuilder::reactor),
|
||||
|
||||
/**
|
||||
* Java's HttpClient.
|
||||
*/
|
||||
JDK(ClientHttpRequestFactoryBuilder::jdk),
|
||||
|
||||
/**
|
||||
* Standard JDK facilities.
|
||||
*/
|
||||
SIMPLE(ClientHttpRequestFactoryBuilder::simple);
|
||||
|
||||
private final Supplier<ClientHttpRequestFactoryBuilder<?>> builderSupplier;
|
||||
|
||||
Factory(Supplier<ClientHttpRequestFactoryBuilder<?>> builderSupplier) {
|
||||
this.builderSupplier = builderSupplier;
|
||||
}
|
||||
|
||||
ClientHttpRequestFactoryBuilder<?> builder() {
|
||||
return this.builderSupplier.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -21,17 +21,17 @@ import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.http.client.HttpClientProperties.Factory;
|
||||
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
|
||||
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.SslBundle;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
|
||||
import org.springframework.boot.http.client.HttpClientSettings;
|
||||
import org.springframework.boot.http.client.HttpRedirects;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for
|
||||
@@ -49,22 +49,24 @@ public class HttpClientAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
ClientHttpRequestFactoryBuilder<?> clientHttpRequestFactoryBuilder(HttpClientProperties httpClientProperties) {
|
||||
Factory factory = httpClientProperties.getFactory();
|
||||
return (factory != null) ? factory.builder() : ClientHttpRequestFactoryBuilder.detect();
|
||||
return httpClientProperties.factoryBuilder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
ClientHttpRequestFactorySettings clientHttpRequestFactorySettings(HttpClientProperties httpClientProperties,
|
||||
ObjectProvider<SslBundles> sslBundles) {
|
||||
SslBundle sslBundle = getSslBundle(httpClientProperties.getSsl(), sslBundles);
|
||||
return new ClientHttpRequestFactorySettings(httpClientProperties.getRedirects(),
|
||||
httpClientProperties.getConnectTimeout(), httpClientProperties.getReadTimeout(), sslBundle);
|
||||
HttpClientSettings settings = httpClientProperties.httpClientSettings(sslBundles);
|
||||
return new ClientHttpRequestFactorySettings(asRequestFactoryRedirects(settings.redirects()),
|
||||
settings.connectTimeout(), settings.readTimeout(), settings.sslBundle());
|
||||
}
|
||||
|
||||
private SslBundle getSslBundle(HttpClientProperties.Ssl properties, ObjectProvider<SslBundles> sslBundles) {
|
||||
String name = properties.getBundle();
|
||||
return (StringUtils.hasLength(name)) ? sslBundles.getObject().getBundle(name) : null;
|
||||
private Redirects asRequestFactoryRedirects(HttpRedirects redirects) {
|
||||
return switch (redirects) {
|
||||
case FOLLOW_WHEN_POSSIBLE -> Redirects.FOLLOW_WHEN_POSSIBLE;
|
||||
case FOLLOW -> Redirects.FOLLOW;
|
||||
case DONT_FOLLOW -> Redirects.DONT_FOLLOW;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -16,12 +16,8 @@
|
||||
|
||||
package org.springframework.boot.autoconfigure.http.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
|
||||
|
||||
/**
|
||||
* {@link ConfigurationProperties @ConfigurationProperties} for a Spring's blocking HTTP
|
||||
@@ -29,131 +25,9 @@ import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Red
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.4.0
|
||||
* @see ClientHttpRequestFactorySettings
|
||||
*/
|
||||
@ConfigurationProperties("spring.http.client")
|
||||
public class HttpClientProperties {
|
||||
|
||||
/**
|
||||
* Default factory used for a client HTTP request.
|
||||
*/
|
||||
private Factory factory;
|
||||
|
||||
/**
|
||||
* Handling for HTTP redirects.
|
||||
*/
|
||||
private Redirects redirects = Redirects.FOLLOW_WHEN_POSSIBLE;
|
||||
|
||||
/**
|
||||
* Default connect timeout for a client HTTP request.
|
||||
*/
|
||||
private Duration connectTimeout;
|
||||
|
||||
/**
|
||||
* Default read timeout for a client HTTP request.
|
||||
*/
|
||||
private Duration readTimeout;
|
||||
|
||||
/**
|
||||
* Default SSL configuration for a client HTTP request.
|
||||
*/
|
||||
private final Ssl ssl = new Ssl();
|
||||
|
||||
public Factory getFactory() {
|
||||
return this.factory;
|
||||
}
|
||||
|
||||
public void setFactory(Factory factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
public Redirects getRedirects() {
|
||||
return this.redirects;
|
||||
}
|
||||
|
||||
public void setRedirects(Redirects redirects) {
|
||||
this.redirects = redirects;
|
||||
}
|
||||
|
||||
public Duration getConnectTimeout() {
|
||||
return this.connectTimeout;
|
||||
}
|
||||
|
||||
public void setConnectTimeout(Duration connectTimeout) {
|
||||
this.connectTimeout = connectTimeout;
|
||||
}
|
||||
|
||||
public Duration getReadTimeout() {
|
||||
return this.readTimeout;
|
||||
}
|
||||
|
||||
public void setReadTimeout(Duration readTimeout) {
|
||||
this.readTimeout = readTimeout;
|
||||
}
|
||||
|
||||
public Ssl getSsl() {
|
||||
return this.ssl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported factory types.
|
||||
*/
|
||||
public enum Factory {
|
||||
|
||||
/**
|
||||
* Apache HttpComponents HttpClient.
|
||||
*/
|
||||
HTTP_COMPONENTS(ClientHttpRequestFactoryBuilder::httpComponents),
|
||||
|
||||
/**
|
||||
* Jetty's HttpClient.
|
||||
*/
|
||||
JETTY(ClientHttpRequestFactoryBuilder::jetty),
|
||||
|
||||
/**
|
||||
* Reactor-Netty.
|
||||
*/
|
||||
REACTOR(ClientHttpRequestFactoryBuilder::reactor),
|
||||
|
||||
/**
|
||||
* Java's HttpClient.
|
||||
*/
|
||||
JDK(ClientHttpRequestFactoryBuilder::jdk),
|
||||
|
||||
/**
|
||||
* Standard JDK facilities.
|
||||
*/
|
||||
SIMPLE(ClientHttpRequestFactoryBuilder::simple);
|
||||
|
||||
private final Supplier<ClientHttpRequestFactoryBuilder<?>> builderSupplier;
|
||||
|
||||
Factory(Supplier<ClientHttpRequestFactoryBuilder<?>> builderSupplier) {
|
||||
this.builderSupplier = builderSupplier;
|
||||
}
|
||||
|
||||
ClientHttpRequestFactoryBuilder<?> builder() {
|
||||
return this.builderSupplier.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* SSL configuration.
|
||||
*/
|
||||
public static class Ssl {
|
||||
|
||||
/**
|
||||
* SSL bundle to use.
|
||||
*/
|
||||
private String bundle;
|
||||
|
||||
public String getBundle() {
|
||||
return this.bundle;
|
||||
}
|
||||
|
||||
public void setBundle(String bundle) {
|
||||
this.bundle = bundle;
|
||||
}
|
||||
|
||||
}
|
||||
public class HttpClientProperties extends AbstractHttpRequestFactoryProperties {
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.util.function.Supplier;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.http.client.AbstractHttpClientProperties;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.http.client.HttpClientSettings;
|
||||
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.http.client.reactive.ClientHttpConnector;
|
||||
|
||||
/**
|
||||
* Base {@link ConfigurationProperties @ConfigurationProperties} for configuring a
|
||||
* {@link ClientHttpConnector}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
* @see ClientHttpConnectorSettings
|
||||
*/
|
||||
public abstract class AbstractClientHttpConnectorProperties extends AbstractHttpClientProperties {
|
||||
|
||||
/**
|
||||
* Default connector used for a client HTTP request.
|
||||
*/
|
||||
private Connector connector;
|
||||
|
||||
public Connector getConnector() {
|
||||
return this.connector;
|
||||
}
|
||||
|
||||
public void setConnector(Connector connector) {
|
||||
this.connector = connector;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpClientSettings httpClientSettings(ObjectProvider<SslBundles> sslBundles) {
|
||||
return super.httpClientSettings(sslBundles);
|
||||
}
|
||||
|
||||
protected final ClientHttpConnectorBuilder<?> connectorBuilder(ClassLoader classLoader) {
|
||||
Connector connector = getConnector();
|
||||
return (connector != null) ? connector.builder() : ClientHttpConnectorBuilder.detect(classLoader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported factory types.
|
||||
*/
|
||||
public enum Connector {
|
||||
|
||||
/**
|
||||
* Reactor-Netty.
|
||||
*/
|
||||
REACTOR(ClientHttpConnectorBuilder::reactor),
|
||||
|
||||
/**
|
||||
* Jetty's HttpClient.
|
||||
*/
|
||||
JETTY(ClientHttpConnectorBuilder::jetty),
|
||||
|
||||
/**
|
||||
* Apache HttpComponents HttpClient.
|
||||
*/
|
||||
HTTP_COMPONENTS(ClientHttpConnectorBuilder::httpComponents),
|
||||
|
||||
/**
|
||||
* Java's HttpClient.
|
||||
*/
|
||||
JDK(ClientHttpConnectorBuilder::jdk);
|
||||
|
||||
private final Supplier<ClientHttpConnectorBuilder<?>> builderSupplier;
|
||||
|
||||
Connector(Supplier<ClientHttpConnectorBuilder<?>> builderSupplier) {
|
||||
this.builderSupplier = builderSupplier;
|
||||
}
|
||||
|
||||
ClientHttpConnectorBuilder<?> builder() {
|
||||
return this.builderSupplier.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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.util.List;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.reactor.netty.ReactorNettyConfigurations;
|
||||
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.http.client.HttpClientSettings;
|
||||
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.util.LambdaSafe;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for
|
||||
* {@link ClientHttpConnectorBuilder} and {@link ClientHttpConnectorSettings}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
*/
|
||||
@AutoConfiguration(after = SslAutoConfiguration.class)
|
||||
@ConditionalOnClass({ ClientHttpConnector.class, Mono.class })
|
||||
@EnableConfigurationProperties(HttpReactiveClientSettingsProperties.class)
|
||||
public class ClientHttpConnectorAutoConfiguration implements BeanClassLoaderAware {
|
||||
|
||||
private ClassLoader beanClassLoader;
|
||||
|
||||
@Override
|
||||
public void setBeanClassLoader(ClassLoader classLoader) {
|
||||
this.beanClassLoader = classLoader;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
ClientHttpConnectorBuilder<?> clientHttpConnectorBuilder(
|
||||
HttpReactiveClientSettingsProperties httpReactiveClientSettingsProperties,
|
||||
ObjectProvider<ClientHttpConnectorBuilderCustomizer<?>> clientHttpConnectorBuilderCustomizers) {
|
||||
ClientHttpConnectorBuilder<?> builder = httpReactiveClientSettingsProperties
|
||||
.connectorBuilder(this.beanClassLoader);
|
||||
return customize(builder, clientHttpConnectorBuilderCustomizers.orderedStream().toList());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ClientHttpConnectorBuilder<?> customize(ClientHttpConnectorBuilder<?> builder,
|
||||
List<ClientHttpConnectorBuilderCustomizer<?>> customizers) {
|
||||
ClientHttpConnectorBuilder<?>[] builderReference = { builder };
|
||||
LambdaSafe.callbacks(ClientHttpConnectorBuilderCustomizer.class, customizers, builderReference[0])
|
||||
.invoke((customizer) -> builderReference[0] = customizer.customize(builderReference[0]));
|
||||
return builderReference[0];
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
ClientHttpConnectorSettings clientHttpConnectorSettings(
|
||||
HttpReactiveClientSettingsProperties httpReactiveClientSettingsProperties,
|
||||
ObjectProvider<SslBundles> sslBundles) {
|
||||
HttpClientSettings settings = httpReactiveClientSettingsProperties.httpClientSettings(sslBundles);
|
||||
return new ClientHttpConnectorSettings(settings.redirects(), settings.connectTimeout(), settings.readTimeout(),
|
||||
settings.sslBundle());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Lazy
|
||||
@ConditionalOnMissingBean
|
||||
ClientHttpConnector clientHttpConnector(ClientHttpConnectorBuilder<?> clientHttpConnectorBuilder,
|
||||
ClientHttpConnectorSettings clientHttpRequestFactorySettings) {
|
||||
return clientHttpConnectorBuilder.build(clientHttpRequestFactorySettings);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(reactor.netty.http.client.HttpClient.class)
|
||||
@Import(ReactorNettyConfigurations.ReactorResourceFactoryConfiguration.class)
|
||||
static class ReactorNetty {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder;
|
||||
|
||||
/**
|
||||
* Customizer that can be used to modify the auto-configured
|
||||
* {@link ClientHttpConnectorBuilder} when its type matches.
|
||||
*
|
||||
* @param <B> the builder type
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public interface ClientHttpConnectorBuilderCustomizer<B extends ClientHttpConnectorBuilder<?>> {
|
||||
|
||||
/**
|
||||
* Customize the given builder.
|
||||
* @param builder the builder to customize
|
||||
* @return the customized builder
|
||||
*/
|
||||
B customize(B builder);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorSettings;
|
||||
|
||||
/**
|
||||
* {@link ConfigurationProperties @ConfigurationProperties} to configure settings that
|
||||
* apply to Spring's reactive client HTTP connectors.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
* @see ClientHttpConnectorSettings
|
||||
*/
|
||||
@ConfigurationProperties("spring.http.reactiveclient.settings")
|
||||
public class HttpReactiveClientSettingsProperties extends AbstractClientHttpConnectorProperties {
|
||||
|
||||
}
|
||||
@@ -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 client-side reactive HTTP.
|
||||
*/
|
||||
package org.springframework.boot.autoconfigure.http.client.reactive;
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -18,6 +18,8 @@ package org.springframework.boot.autoconfigure.web.reactive.function.client;
|
||||
|
||||
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;
|
||||
@@ -30,12 +32,16 @@ import org.springframework.web.reactive.function.client.WebClient;
|
||||
*/
|
||||
class AutoConfiguredWebClientSsl implements WebClientSsl {
|
||||
|
||||
private final ClientHttpConnectorFactory<?> clientHttpConnectorFactory;
|
||||
private final ClientHttpConnectorBuilder<?> connectorBuilder;
|
||||
|
||||
private final ClientHttpConnectorSettings settings;
|
||||
|
||||
private final SslBundles sslBundles;
|
||||
|
||||
AutoConfiguredWebClientSsl(ClientHttpConnectorFactory<?> clientHttpConnectorFactory, SslBundles sslBundles) {
|
||||
this.clientHttpConnectorFactory = clientHttpConnectorFactory;
|
||||
AutoConfiguredWebClientSsl(ClientHttpConnectorBuilder<?> connectorBuilder, ClientHttpConnectorSettings settings,
|
||||
SslBundles sslBundles) {
|
||||
this.connectorBuilder = connectorBuilder;
|
||||
this.settings = settings;
|
||||
this.sslBundles = sslBundles;
|
||||
}
|
||||
|
||||
@@ -47,7 +53,8 @@ class AutoConfiguredWebClientSsl implements WebClientSsl {
|
||||
@Override
|
||||
public Consumer<WebClient.Builder> fromBundle(SslBundle bundle) {
|
||||
return (builder) -> {
|
||||
ClientHttpConnector connector = this.clientHttpConnectorFactory.createClientHttpConnector(bundle);
|
||||
ClientHttpConnectorSettings settings = this.settings.withSslBundle(bundle);
|
||||
ClientHttpConnector connector = this.connectorBuilder.build(settings);
|
||||
builder.clientConnector(connector);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -16,53 +16,64 @@
|
||||
|
||||
package org.springframework.boot.autoconfigure.web.reactive.function.client;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
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.autoconfigure.ssl.SslAutoConfiguration;
|
||||
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
|
||||
import org.springframework.boot.autoconfigure.http.client.reactive.ClientHttpConnectorBuilderCustomizer;
|
||||
import org.springframework.boot.autoconfigure.reactor.netty.ReactorNettyConfigurations.ReactorResourceFactoryConfiguration;
|
||||
import org.springframework.boot.http.client.reactive.ReactorClientHttpConnectorBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.http.client.ReactorResourceFactory;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for {@link ClientHttpConnector}.
|
||||
* <p>
|
||||
* It can produce a {@link org.springframework.http.client.reactive.ClientHttpConnector}
|
||||
* bean and possibly a companion {@code ResourceFactory} bean, depending on the chosen
|
||||
* HTTP client library.
|
||||
* Deprecated {@link EnableAutoConfiguration Auto-configuration} for
|
||||
* {@link ReactorNettyHttpClientMapper}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Phillip Webb
|
||||
* @since 2.1.0
|
||||
* @deprecated since 3.5.0 for removal in 3.7.0 in favor of
|
||||
* {@link org.springframework.boot.autoconfigure.http.client.reactive.ClientHttpConnectorAutoConfiguration}
|
||||
* and to align with the deprecation of {@link ReactorNettyHttpClientMapper}
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass(WebClient.class)
|
||||
@AutoConfigureAfter(SslAutoConfiguration.class)
|
||||
@Import({ ClientHttpConnectorFactoryConfiguration.ReactorNetty.class,
|
||||
ClientHttpConnectorFactoryConfiguration.HttpClient5.class,
|
||||
ClientHttpConnectorFactoryConfiguration.JdkClient.class })
|
||||
@Deprecated(since = "3.5.0", forRemoval = true)
|
||||
public class ClientHttpConnectorAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@Lazy
|
||||
@ConditionalOnMissingBean
|
||||
ClientHttpConnector webClientHttpConnector(ClientHttpConnectorFactory<?> clientHttpConnectorFactory) {
|
||||
return clientHttpConnectorFactory.createClientHttpConnector();
|
||||
}
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(HttpClient.class)
|
||||
@Import(ReactorResourceFactoryConfiguration.class)
|
||||
@SuppressWarnings("removal")
|
||||
static class ReactorNetty {
|
||||
|
||||
@Bean
|
||||
@Order(0)
|
||||
ClientHttpConnectorBuilderCustomizer<ReactorClientHttpConnectorBuilder> reactorNettyHttpClientMapperClientHttpConnectorBuilderCustomizer(
|
||||
ReactorResourceFactory reactorResourceFactory,
|
||||
ObjectProvider<ReactorNettyHttpClientMapper> mapperProvider) {
|
||||
return applyMappers(mapperProvider.orderedStream().toList());
|
||||
}
|
||||
|
||||
private ClientHttpConnectorBuilderCustomizer<ReactorClientHttpConnectorBuilder> applyMappers(
|
||||
List<ReactorNettyHttpClientMapper> mappers) {
|
||||
return (builder) -> {
|
||||
for (ReactorNettyHttpClientMapper mapper : mappers) {
|
||||
builder = builder.withHttpClientCustomizer(mapper::configure);
|
||||
}
|
||||
return builder;
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Lazy
|
||||
@Order(0)
|
||||
@ConditionalOnBean(ClientHttpConnector.class)
|
||||
public WebClientCustomizer webClientHttpConnectorCustomizer(ClientHttpConnector clientHttpConnector) {
|
||||
return (builder) -> builder.clientConnector(clientHttpConnector);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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.web.reactive.function.client;
|
||||
|
||||
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
|
||||
import org.apache.hc.core5.http.nio.AsyncRequestProducer;
|
||||
import org.apache.hc.core5.reactive.ReactiveResponseConsumer;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.reactor.netty.ReactorNettyConfigurations;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.client.ReactorResourceFactory;
|
||||
|
||||
/**
|
||||
* Configuration classes for WebClient client connectors.
|
||||
* <p>
|
||||
* Those should be {@code @Import} in a regular auto-configuration class to guarantee
|
||||
* their order of execution.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
class ClientHttpConnectorFactoryConfiguration {
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(reactor.netty.http.client.HttpClient.class)
|
||||
@ConditionalOnMissingBean(ClientHttpConnectorFactory.class)
|
||||
@Import(ReactorNettyConfigurations.ReactorResourceFactoryConfiguration.class)
|
||||
static class ReactorNetty {
|
||||
|
||||
@Bean
|
||||
ReactorClientHttpConnectorFactory reactorClientHttpConnectorFactory(
|
||||
ReactorResourceFactory reactorResourceFactory,
|
||||
ObjectProvider<ReactorNettyHttpClientMapper> mapperProvider) {
|
||||
return new ReactorClientHttpConnectorFactory(reactorResourceFactory, mapperProvider::orderedStream);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass({ HttpAsyncClients.class, AsyncRequestProducer.class, ReactiveResponseConsumer.class })
|
||||
@ConditionalOnMissingBean(ClientHttpConnectorFactory.class)
|
||||
static class HttpClient5 {
|
||||
|
||||
@Bean
|
||||
HttpComponentsClientHttpConnectorFactory httpComponentsClientHttpConnectorFactory() {
|
||||
return new HttpComponentsClientHttpConnectorFactory();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(java.net.http.HttpClient.class)
|
||||
@ConditionalOnMissingBean(ClientHttpConnectorFactory.class)
|
||||
static class JdkClient {
|
||||
|
||||
@Bean
|
||||
JdkClientHttpConnectorFactory jdkClientHttpConnectorFactory() {
|
||||
return new JdkClientHttpConnectorFactory();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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.web.reactive.function.client;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
|
||||
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
|
||||
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
|
||||
import org.apache.hc.client5.http.nio.AsyncClientConnectionManager;
|
||||
import org.apache.hc.core5.http.nio.ssl.BasicClientTlsStrategy;
|
||||
import org.apache.hc.core5.reactor.ssl.SSLSessionVerifier;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector;
|
||||
|
||||
/**
|
||||
* {@link ClientHttpConnectorFactory} for {@link HttpComponentsClientHttpConnector}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class HttpComponentsClientHttpConnectorFactory
|
||||
implements ClientHttpConnectorFactory<HttpComponentsClientHttpConnector> {
|
||||
|
||||
@Override
|
||||
public HttpComponentsClientHttpConnector createClientHttpConnector(SslBundle sslBundle) {
|
||||
HttpAsyncClientBuilder builder = HttpAsyncClients.custom().useSystemProperties();
|
||||
if (sslBundle != null) {
|
||||
SslOptions options = sslBundle.getOptions();
|
||||
SSLContext sslContext = sslBundle.createSslContext();
|
||||
SSLSessionVerifier sessionVerifier = (endpoint, sslEngine) -> {
|
||||
if (options.getCiphers() != null) {
|
||||
sslEngine.setEnabledCipherSuites(options.getCiphers());
|
||||
}
|
||||
if (options.getEnabledProtocols() != null) {
|
||||
sslEngine.setEnabledProtocols(options.getEnabledProtocols());
|
||||
}
|
||||
return null;
|
||||
};
|
||||
BasicClientTlsStrategy tlsStrategy = new BasicClientTlsStrategy(sslContext, sessionVerifier);
|
||||
AsyncClientConnectionManager connectionManager = PoolingAsyncClientConnectionManagerBuilder.create()
|
||||
.setTlsStrategy(tlsStrategy)
|
||||
.build();
|
||||
builder.setConnectionManager(connectionManager);
|
||||
}
|
||||
return new HttpComponentsClientHttpConnector(builder.build());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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.web.reactive.function.client;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpClient.Builder;
|
||||
|
||||
import javax.net.ssl.SSLParameters;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.http.client.reactive.JdkClientHttpConnector;
|
||||
|
||||
/**
|
||||
* {@link ClientHttpConnectorFactory} for {@link JdkClientHttpConnector}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class JdkClientHttpConnectorFactory implements ClientHttpConnectorFactory<JdkClientHttpConnector> {
|
||||
|
||||
@Override
|
||||
public JdkClientHttpConnector createClientHttpConnector(SslBundle sslBundle) {
|
||||
Builder builder = HttpClient.newBuilder();
|
||||
if (sslBundle != null) {
|
||||
SslOptions options = sslBundle.getOptions();
|
||||
builder.sslContext(sslBundle.createSslContext());
|
||||
SSLParameters parameters = new SSLParameters();
|
||||
parameters.setCipherSuites(options.getCiphers());
|
||||
parameters.setProtocols(options.getEnabledProtocols());
|
||||
builder.sslParameters(parameters);
|
||||
}
|
||||
return new JdkClientHttpConnector(builder.build());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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.web.reactive.function.client;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
|
||||
import io.netty.handler.ssl.SslContextBuilder;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
import reactor.netty.tcp.SslProvider.SslContextSpec;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslManagerBundle;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.http.client.ReactorResourceFactory;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.util.function.ThrowingConsumer;
|
||||
|
||||
/**
|
||||
* {@link ClientHttpConnectorFactory} for {@link ReactorClientHttpConnector}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Fernando Cappi
|
||||
*/
|
||||
class ReactorClientHttpConnectorFactory implements ClientHttpConnectorFactory<ReactorClientHttpConnector> {
|
||||
|
||||
private final ReactorResourceFactory reactorResourceFactory;
|
||||
|
||||
private final Supplier<Stream<ReactorNettyHttpClientMapper>> mappers;
|
||||
|
||||
ReactorClientHttpConnectorFactory(ReactorResourceFactory reactorResourceFactory) {
|
||||
this(reactorResourceFactory, Stream::empty);
|
||||
}
|
||||
|
||||
ReactorClientHttpConnectorFactory(ReactorResourceFactory reactorResourceFactory,
|
||||
Supplier<Stream<ReactorNettyHttpClientMapper>> mappers) {
|
||||
this.reactorResourceFactory = reactorResourceFactory;
|
||||
this.mappers = mappers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactorClientHttpConnector createClientHttpConnector(SslBundle sslBundle) {
|
||||
List<ReactorNettyHttpClientMapper> mappers = this.mappers.get()
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
if (sslBundle != null) {
|
||||
mappers.add(new SslConfigurer(sslBundle));
|
||||
}
|
||||
return new ReactorClientHttpConnector(this.reactorResourceFactory,
|
||||
ReactorNettyHttpClientMapper.of(mappers)::configure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the Netty {@link HttpClient} with SSL.
|
||||
*/
|
||||
private static class SslConfigurer implements ReactorNettyHttpClientMapper {
|
||||
|
||||
private final SslBundle sslBundle;
|
||||
|
||||
SslConfigurer(SslBundle sslBundle) {
|
||||
this.sslBundle = sslBundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpClient configure(HttpClient httpClient) {
|
||||
return httpClient.secure(ThrowingConsumer.of(this::customizeSsl).throwing(IllegalStateException::new));
|
||||
}
|
||||
|
||||
private void customizeSsl(SslContextSpec spec) throws SSLException {
|
||||
SslOptions options = this.sslBundle.getOptions();
|
||||
SslManagerBundle managers = this.sslBundle.getManagers();
|
||||
SslContextBuilder builder = SslContextBuilder.forClient()
|
||||
.keyManager(managers.getKeyManagerFactory())
|
||||
.trustManager(managers.getTrustManagerFactory())
|
||||
.ciphers(SslOptions.asSet(options.getCiphers()))
|
||||
.protocols(options.getEnabledProtocols());
|
||||
spec.sslContext(builder.build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,6 +20,8 @@ import java.util.Collection;
|
||||
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
|
||||
import org.springframework.boot.autoconfigure.http.client.reactive.ClientHttpConnectorBuilderCustomizer;
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -30,8 +32,12 @@ import org.springframework.util.Assert;
|
||||
* @author Brian Clozel
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
* @deprecated since 3.5.0 for removal in 3.7.0 in favor of
|
||||
* {@link ClientHttpConnectorBuilderCustomizer} or declaring a pre-configured
|
||||
* {@link ClientHttpConnectorBuilder} bean
|
||||
*/
|
||||
@FunctionalInterface
|
||||
@Deprecated(since = "3.5.0", forRemoval = true)
|
||||
public interface ReactorNettyHttpClientMapper {
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -23,14 +23,19 @@ 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.autoconfigure.http.client.reactive.ClientHttpConnectorAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration;
|
||||
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.codec.CodecCustomizer;
|
||||
import org.springframework.boot.web.reactive.function.client.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;
|
||||
|
||||
/**
|
||||
@@ -42,6 +47,7 @@ import org.springframework.web.reactive.function.client.WebClient;
|
||||
* will receive a newly cloned instance of the builder.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Phillip Webb
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@AutoConfiguration(after = { CodecsAutoConfiguration.class, ClientHttpConnectorAutoConfiguration.class })
|
||||
@@ -57,12 +63,20 @@ public class WebClientAutoConfiguration {
|
||||
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(ClientHttpConnectorFactory<?> clientHttpConnectorFactory,
|
||||
SslBundles sslBundles) {
|
||||
return new AutoConfiguredWebClientSsl(clientHttpConnectorFactory, sslBundles);
|
||||
AutoConfiguredWebClientSsl webClientSsl(ClientHttpConnectorBuilder<?> clientHttpConnectorBuilder,
|
||||
ClientHttpConnectorSettings clientHttpConnectorSettings, SslBundles sslBundles) {
|
||||
return new AutoConfiguredWebClientSsl(clientHttpConnectorBuilder, clientHttpConnectorSettings, sslBundles);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
|
||||
@@ -63,6 +63,7 @@ org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration
|
||||
org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration
|
||||
org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration
|
||||
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.info.ProjectInfoAutoConfiguration
|
||||
org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -19,7 +19,7 @@ package org.springframework.boot.autoconfigure.http.client;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.http.client.HttpClientProperties.Factory;
|
||||
import org.springframework.boot.autoconfigure.http.client.AbstractHttpRequestFactoryProperties.Factory;
|
||||
import org.springframework.boot.http.client.HttpComponentsClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.JdkClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.JettyClientHttpRequestFactoryBuilder;
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
|
||||
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.JdkClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.JettyClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.ReactorClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.ReactorResourceFactory;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link ClientHttpConnectorAutoConfiguration}
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ClientHttpConnectorAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(
|
||||
AutoConfigurations.of(ClientHttpConnectorAutoConfiguration.class, SslAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void whenReactorIsAvailableThenReactorBeansAreDefined() {
|
||||
this.contextRunner.run((context) -> {
|
||||
BeanDefinition connectorDefinition = context.getBeanFactory().getBeanDefinition("clientHttpConnector");
|
||||
assertThat(connectorDefinition.isLazyInit()).isTrue();
|
||||
assertThat(context).hasSingleBean(ReactorResourceFactory.class);
|
||||
assertThat(context.getBean(ClientHttpConnectorBuilder.class))
|
||||
.isExactlyInstanceOf(ReactorClientHttpConnectorBuilder.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenReactorIsUnavailableThenJettyClientBeansAreDefined() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(HttpClient.class)).run((context) -> {
|
||||
BeanDefinition connectorDefinition = context.getBeanFactory().getBeanDefinition("clientHttpConnector");
|
||||
assertThat(connectorDefinition.isLazyInit()).isTrue();
|
||||
assertThat(context.getBean(ClientHttpConnectorBuilder.class))
|
||||
.isExactlyInstanceOf(JettyClientHttpConnectorBuilder.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenReactorAndHttpClientAreUnavailableThenJettyClientBeansAreDefined() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(HttpClient.class, HttpAsyncClients.class))
|
||||
.run((context) -> {
|
||||
BeanDefinition connectorDefinition = context.getBeanFactory().getBeanDefinition("clientHttpConnector");
|
||||
assertThat(connectorDefinition.isLazyInit()).isTrue();
|
||||
assertThat(context.getBean(ClientHttpConnectorBuilder.class))
|
||||
.isExactlyInstanceOf(JettyClientHttpConnectorBuilder.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenReactorAndHttpClientAndJettyAreUnavailableThenJdkClientBeansAreDefined() {
|
||||
this.contextRunner
|
||||
.withClassLoader(new FilteredClassLoader(HttpClient.class, HttpAsyncClients.class,
|
||||
org.eclipse.jetty.client.HttpClient.class))
|
||||
.run((context) -> {
|
||||
BeanDefinition connectorDefinition = context.getBeanFactory().getBeanDefinition("clientHttpConnector");
|
||||
assertThat(connectorDefinition.isLazyInit()).isTrue();
|
||||
assertThat(context.getBean(ClientHttpConnectorBuilder.class))
|
||||
.isExactlyInstanceOf(JdkClientHttpConnectorBuilder.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotOverrideCustomClientConnector() {
|
||||
this.contextRunner.withUserConfiguration(CustomClientHttpConnectorConfig.class).run((context) -> {
|
||||
assertThat(context).hasSingleBean(ClientHttpConnector.class);
|
||||
assertThat(context).hasBean("customConnector");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseCustomReactorResourceFactory() {
|
||||
this.contextRunner.withUserConfiguration(CustomReactorResourceConfig.class).run((context) -> {
|
||||
assertThat(context).hasSingleBean(ClientHttpConnector.class);
|
||||
assertThat(context).hasSingleBean(ReactorResourceFactory.class);
|
||||
assertThat(context).hasBean("customReactorResourceFactory");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void configuresDetectedClientHttpConnectorBuilderBuilder() {
|
||||
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ClientHttpConnectorBuilder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void configuresDefinedClientHttpConnectorBuilder() {
|
||||
this.contextRunner.withPropertyValues("spring.http.reactiveclient.settings.connector=jetty")
|
||||
.run((context) -> assertThat(context.getBean(ClientHttpConnectorBuilder.class))
|
||||
.isInstanceOf(JettyClientHttpConnectorBuilder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void configuresClientHttpConnectorSettings() {
|
||||
this.contextRunner.withPropertyValues(sslPropertyValues().toArray(String[]::new))
|
||||
.withPropertyValues("spring.http.reactiveclient.settings.redirects=dont-follow",
|
||||
"spring.http.reactiveclient.settings.connect-timeout=10s",
|
||||
"spring.http.reactiveclient.settings.read-timeout=20s",
|
||||
"spring.http.reactiveclient.settings.ssl.bundle=test")
|
||||
.run((context) -> {
|
||||
ClientHttpConnectorSettings settings = context.getBean(ClientHttpConnectorSettings.class);
|
||||
assertThat(settings.redirects()).isEqualTo(HttpRedirects.DONT_FOLLOW);
|
||||
assertThat(settings.connectTimeout()).isEqualTo(Duration.ofSeconds(10));
|
||||
assertThat(settings.readTimeout()).isEqualTo(Duration.ofSeconds(20));
|
||||
assertThat(settings.sslBundle().getKey().getAlias()).isEqualTo("alias1");
|
||||
});
|
||||
}
|
||||
|
||||
private List<String> sslPropertyValues() {
|
||||
List<String> propertyValues = new ArrayList<>();
|
||||
String location = "classpath:org/springframework/boot/autoconfigure/ssl/";
|
||||
propertyValues.add("spring.ssl.bundle.pem.test.key.alias=alias1");
|
||||
propertyValues.add("spring.ssl.bundle.pem.test.truststore.type=PKCS12");
|
||||
propertyValues.add("spring.ssl.bundle.pem.test.truststore.certificate=" + location + "rsa-cert.pem");
|
||||
propertyValues.add("spring.ssl.bundle.pem.test.truststore.private-key=" + location + "rsa-key.pem");
|
||||
return propertyValues;
|
||||
}
|
||||
|
||||
@Test
|
||||
void clientHttpConnectorBuilderCustomizersAreApplied() {
|
||||
this.contextRunner.withPropertyValues("spring.http.reactiveclient.settings.connector=jdk")
|
||||
.withUserConfiguration(ClientHttpConnectorBuilderCustomizersConfiguration.class)
|
||||
.run((context) -> {
|
||||
ClientHttpConnector connector = context.getBean(ClientHttpConnectorBuilder.class).build();
|
||||
assertThat(connector).extracting("readTimeout").isEqualTo(Duration.ofSeconds(5));
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomClientHttpConnectorConfig {
|
||||
|
||||
@Bean
|
||||
ClientHttpConnector customConnector() {
|
||||
return mock(ClientHttpConnector.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomReactorResourceConfig {
|
||||
|
||||
@Bean
|
||||
ReactorResourceFactory customReactorResourceFactory() {
|
||||
return new ReactorResourceFactory();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ClientHttpConnectorBuilderCustomizersConfiguration {
|
||||
|
||||
@Bean
|
||||
ClientHttpConnectorBuilderCustomizer<JdkClientHttpConnectorBuilder> jdkCustomizer() {
|
||||
return (builder) -> builder.withCustomizer((connector) -> connector.setReadTimeout(Duration.ofSeconds(5)));
|
||||
}
|
||||
|
||||
@Bean
|
||||
ClientHttpConnectorBuilderCustomizer<JettyClientHttpConnectorBuilder> jettyCustomizer() {
|
||||
return (builder) -> {
|
||||
throw new IllegalStateException();
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.http.client.reactive.AbstractClientHttpConnectorProperties.Connector;
|
||||
import org.springframework.boot.http.client.reactive.HttpComponentsClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.JdkClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.JettyClientHttpConnectorBuilder;
|
||||
import org.springframework.boot.http.client.reactive.ReactorClientHttpConnectorBuilder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpReactiveClientSettingsProperties}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class HttpReactiveClientSettingsPropertiesTests {
|
||||
|
||||
@Nested
|
||||
class ConnectorTests {
|
||||
|
||||
@Test
|
||||
void reactorBuilder() {
|
||||
assertThat(Connector.REACTOR.builder()).isInstanceOf(ReactorClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void jettyBuilder() {
|
||||
assertThat(Connector.JETTY.builder()).isInstanceOf(JettyClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void httpComponentsBuilder() {
|
||||
assertThat(Connector.HTTP_COMPONENTS.builder())
|
||||
.isInstanceOf(HttpComponentsClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void jdkBuilder() {
|
||||
assertThat(Connector.JDK.builder()).isInstanceOf(JdkClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
/*
|
||||
* 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.web.reactive.function.client;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundleKey;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.Ssl.ClientAuth;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.client.WebClientRequestException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Abstract base class for {@link ClientHttpConnectorFactory} tests.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
abstract class AbstractClientHttpConnectorFactoryTests {
|
||||
|
||||
@Test
|
||||
void insecureConnection() {
|
||||
TomcatServletWebServerFactory webServerFactory = new TomcatServletWebServerFactory(0);
|
||||
WebServer webServer = webServerFactory.getWebServer();
|
||||
try {
|
||||
webServer.start();
|
||||
int port = webServer.getPort();
|
||||
String url = "http://localhost:%s".formatted(port);
|
||||
WebClient insecureWebClient = WebClient.builder()
|
||||
.clientConnector(getFactory().createClientHttpConnector())
|
||||
.build();
|
||||
String insecureBody = insecureWebClient.get()
|
||||
.uri(url)
|
||||
.exchangeToMono((response) -> response.bodyToMono(String.class))
|
||||
.block();
|
||||
assertThat(insecureBody).contains("HTTP Status 404 – Not Found");
|
||||
}
|
||||
finally {
|
||||
webServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void secureConnection() throws Exception {
|
||||
TomcatServletWebServerFactory webServerFactory = new TomcatServletWebServerFactory(0);
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setClientAuth(ClientAuth.NEED);
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setKeyStore("classpath:test.jks");
|
||||
ssl.setTrustStore("classpath:test.jks");
|
||||
webServerFactory.setSsl(ssl);
|
||||
WebServer webServer = webServerFactory.getWebServer();
|
||||
try {
|
||||
webServer.start();
|
||||
int port = webServer.getPort();
|
||||
String url = "https://localhost:%s".formatted(port);
|
||||
WebClient insecureWebClient = WebClient.builder()
|
||||
.clientConnector(getFactory().createClientHttpConnector())
|
||||
.build();
|
||||
assertThatExceptionOfType(WebClientRequestException.class).isThrownBy(() -> insecureWebClient.get()
|
||||
.uri(url)
|
||||
.exchangeToMono((response) -> response.bodyToMono(String.class))
|
||||
.block());
|
||||
JksSslStoreDetails storeDetails = JksSslStoreDetails.forLocation("classpath:test.jks");
|
||||
JksSslStoreBundle stores = new JksSslStoreBundle(storeDetails, storeDetails);
|
||||
SslBundle sslBundle = SslBundle.of(stores, SslBundleKey.of("password"));
|
||||
WebClient secureWebClient = WebClient.builder()
|
||||
.clientConnector(getFactory().createClientHttpConnector(sslBundle))
|
||||
.build();
|
||||
String secureBody = secureWebClient.get()
|
||||
.uri(url)
|
||||
.exchangeToMono((response) -> response.bodyToMono(String.class))
|
||||
.block();
|
||||
assertThat(secureBody).contains("HTTP Status 404 – Not Found");
|
||||
}
|
||||
finally {
|
||||
webServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract ClientHttpConnectorFactory<?> getFactory();
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -16,146 +16,45 @@
|
||||
|
||||
package org.springframework.boot.autoconfigure.web.reactive.function.client;
|
||||
|
||||
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.ReactorResourceFactory;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
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 ClientHttpConnectorAutoConfiguration}
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
class ClientHttpConnectorAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(ClientHttpConnectorAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void whenReactorIsAvailableThenReactorBeansAreDefined() {
|
||||
this.contextRunner.run((context) -> {
|
||||
BeanDefinition customizerDefinition = context.getBeanFactory()
|
||||
.getBeanDefinition("webClientHttpConnectorCustomizer");
|
||||
assertThat(customizerDefinition.isLazyInit()).isTrue();
|
||||
BeanDefinition connectorDefinition = context.getBeanFactory().getBeanDefinition("webClientHttpConnector");
|
||||
assertThat(connectorDefinition.isLazyInit()).isTrue();
|
||||
assertThat(context).hasBean("reactorClientHttpConnectorFactory");
|
||||
assertThat(context).hasSingleBean(ReactorResourceFactory.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenReactorIsUnavailableThenHttpClientBeansAreDefined() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(HttpClient.class)).run((context) -> {
|
||||
BeanDefinition customizerDefinition = context.getBeanFactory()
|
||||
.getBeanDefinition("webClientHttpConnectorCustomizer");
|
||||
assertThat(customizerDefinition.isLazyInit()).isTrue();
|
||||
BeanDefinition connectorDefinition = context.getBeanFactory().getBeanDefinition("webClientHttpConnector");
|
||||
assertThat(connectorDefinition.isLazyInit()).isTrue();
|
||||
assertThat(context).hasBean("httpComponentsClientHttpConnectorFactory");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenReactorAndHttpClientBeansAreUnavailableThenJdkClientBeansAreDefined() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(HttpClient.class, HttpAsyncClients.class))
|
||||
void shouldApplyReactorNettyHttpClientMapper() {
|
||||
new ReactiveWebApplicationContextRunner().withConfiguration(AutoConfigurations.of(
|
||||
ClientHttpConnectorAutoConfiguration.class,
|
||||
org.springframework.boot.autoconfigure.http.client.reactive.ClientHttpConnectorAutoConfiguration.class))
|
||||
.withUserConfiguration(CustomReactorNettyHttpClientMapper.class)
|
||||
.run((context) -> {
|
||||
BeanDefinition customizerDefinition = context.getBeanFactory()
|
||||
.getBeanDefinition("webClientHttpConnectorCustomizer");
|
||||
assertThat(customizerDefinition.isLazyInit()).isTrue();
|
||||
BeanDefinition connectorDefinition = context.getBeanFactory()
|
||||
.getBeanDefinition("webClientHttpConnector");
|
||||
assertThat(connectorDefinition.isLazyInit()).isTrue();
|
||||
assertThat(context).hasBean("jdkClientHttpConnectorFactory");
|
||||
context.getBean(ClientHttpConnector.class);
|
||||
assertThat(CustomReactorNettyHttpClientMapper.called).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateHttpClientBeans() {
|
||||
this.contextRunner.run((context) -> {
|
||||
assertThat(context).hasSingleBean(ReactorResourceFactory.class);
|
||||
assertThat(context).hasSingleBean(ClientHttpConnector.class);
|
||||
WebClientCustomizer clientCustomizer = context.getBean(WebClientCustomizer.class);
|
||||
WebClient.Builder builder = mock(WebClient.Builder.class);
|
||||
clientCustomizer.customize(builder);
|
||||
then(builder).should().clientConnector(any(ReactorClientHttpConnector.class));
|
||||
});
|
||||
}
|
||||
static class CustomReactorNettyHttpClientMapper {
|
||||
|
||||
@Test
|
||||
void shouldNotOverrideCustomClientConnector() {
|
||||
this.contextRunner.withUserConfiguration(CustomClientHttpConnectorConfig.class).run((context) -> {
|
||||
assertThat(context).hasSingleBean(ClientHttpConnector.class).hasBean("customConnector");
|
||||
WebClientCustomizer clientCustomizer = context.getBean(WebClientCustomizer.class);
|
||||
WebClient.Builder builder = mock(WebClient.Builder.class);
|
||||
clientCustomizer.customize(builder);
|
||||
then(builder).should().clientConnector(any(ClientHttpConnector.class));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotOverrideCustomClientConnectorFactory() {
|
||||
this.contextRunner.withUserConfiguration(CustomClientHttpConnectorFactoryConfig.class).run((context) -> {
|
||||
assertThat(context).hasSingleBean(ClientHttpConnectorFactory.class)
|
||||
.hasBean("customConnector")
|
||||
.doesNotHaveBean(ReactorResourceFactory.class);
|
||||
WebClientCustomizer clientCustomizer = context.getBean(WebClientCustomizer.class);
|
||||
WebClient.Builder builder = mock(WebClient.Builder.class);
|
||||
clientCustomizer.customize(builder);
|
||||
then(builder).should().clientConnector(any(ClientHttpConnector.class));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseCustomReactorResourceFactory() {
|
||||
this.contextRunner.withUserConfiguration(CustomReactorResourceConfig.class)
|
||||
.run((context) -> assertThat(context).hasSingleBean(ClientHttpConnector.class)
|
||||
.hasSingleBean(ReactorResourceFactory.class)
|
||||
.hasBean("customReactorResourceFactory"));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomClientHttpConnectorConfig {
|
||||
static boolean called = false;
|
||||
|
||||
@Bean
|
||||
ClientHttpConnector customConnector() {
|
||||
return mock(ClientHttpConnector.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomClientHttpConnectorFactoryConfig {
|
||||
|
||||
@Bean
|
||||
ClientHttpConnectorFactory<?> customConnector() {
|
||||
return (sslBundle) -> mock(ClientHttpConnector.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomReactorResourceConfig {
|
||||
|
||||
@Bean
|
||||
ReactorResourceFactory customReactorResourceFactory() {
|
||||
return new ReactorResourceFactory();
|
||||
ReactorNettyHttpClientMapper clientMapper() {
|
||||
return (client) -> {
|
||||
called = true;
|
||||
return client.baseUrl("/test");
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* 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.web.reactive.function.client;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundleKey;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
||||
/**
|
||||
* Tests for {@link ClientHttpConnectorFactoryConfiguration}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Brian Clozel
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class ClientHttpConnectorFactoryConfigurationTests {
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void shouldApplyHttpClientMapper() {
|
||||
JksSslStoreDetails storeDetails = JksSslStoreDetails.forLocation("classpath:test.jks");
|
||||
JksSslStoreBundle stores = new JksSslStoreBundle(storeDetails, storeDetails);
|
||||
SslBundle sslBundle = spy(SslBundle.of(stores, SslBundleKey.of("password")));
|
||||
new ReactiveWebApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(ClientHttpConnectorFactoryConfiguration.ReactorNetty.class))
|
||||
.withUserConfiguration(CustomHttpClientMapper.class)
|
||||
.run((context) -> {
|
||||
context.getBean(ReactorClientHttpConnectorFactory.class).createClientHttpConnector(sslBundle);
|
||||
assertThat(CustomHttpClientMapper.called).isTrue();
|
||||
then(sslBundle).should().getManagers();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotConfigureReactiveHttpClient5WhenHttpCore5ReactiveJarIsMissing() {
|
||||
new ReactiveWebApplicationContextRunner()
|
||||
.withClassLoader(new FilteredClassLoader("org.apache.hc.core5.reactive"))
|
||||
.withConfiguration(AutoConfigurations.of(ClientHttpConnectorFactoryConfiguration.HttpClient5.class))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(HttpComponentsClientHttpConnector.class));
|
||||
}
|
||||
|
||||
static class CustomHttpClientMapper {
|
||||
|
||||
static boolean called = false;
|
||||
|
||||
@Bean
|
||||
ReactorNettyHttpClientMapper clientMapper() {
|
||||
return (client) -> {
|
||||
called = true;
|
||||
return client.baseUrl("/test");
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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.web.reactive.function.client;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
import org.springframework.http.client.ReactorResourceFactory;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReactorClientHttpConnectorFactory}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ReactorClientHttpConnectorFactoryTests extends AbstractClientHttpConnectorFactoryTests {
|
||||
|
||||
private ReactorResourceFactory resourceFactory;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.resourceFactory = new ReactorResourceFactory();
|
||||
this.resourceFactory.afterPropertiesSet();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void teardown() {
|
||||
this.resourceFactory.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ClientHttpConnectorFactory<?> getFactory() {
|
||||
return new ReactorClientHttpConnectorFactory(this.resourceFactory);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
class ReactorNettyHttpClientMapperTests {
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -41,7 +41,8 @@ import static org.mockito.Mockito.mock;
|
||||
class WebClientAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(ClientHttpConnectorAutoConfiguration.class,
|
||||
.withConfiguration(AutoConfigurations.of(
|
||||
org.springframework.boot.autoconfigure.http.client.reactive.ClientHttpConnectorAutoConfiguration.class,
|
||||
WebClientAutoConfiguration.class, SslAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
|
||||
Binary file not shown.
@@ -52,6 +52,45 @@ You can learn more about the {url-spring-framework-docs}/web/webflux-webclient/c
|
||||
|
||||
|
||||
|
||||
[[io.rest-client.webclient.configuration]]
|
||||
=== Global HTTP Connector Configuration
|
||||
|
||||
If the auto-detected javadoc:org.springframework.http.client.reactive.ClientHttpConnector[] does not meet your needs, you can use the configprop:spring.http.reactiveclient.settings.connector[] property to pick a specific connector.
|
||||
For example, if you have Reactor Netty on your classpath, but you prefer Jetty's javadoc:org.eclipse.jetty.client.HttpClient[] you can add the following:
|
||||
|
||||
[configprops,yaml]
|
||||
----
|
||||
spring:
|
||||
http:
|
||||
reactiveclient:
|
||||
settings:
|
||||
connector: jetty
|
||||
----
|
||||
|
||||
You can also set properties to change defaults that will be applied to all reactive connectors.
|
||||
For example, you may want to change timeouts and if redirects are followed:
|
||||
|
||||
[configprops,yaml]
|
||||
----
|
||||
spring:
|
||||
http:
|
||||
reactiveclient:
|
||||
settings:
|
||||
connect-timeout: 2s
|
||||
read-timeout: 1s
|
||||
redirects: dont-follow
|
||||
----
|
||||
|
||||
For more complex customizations, you can use javadoc:org.springframework.boot.autoconfigure.http.client.reactive.ClientHttpConnectorBuilderCustomizer[] or declare your own javadoc:org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder[] bean which will cause auto-configuration to back off.
|
||||
This can be useful when you need to customize some of the internals of the underlying HTTP library.
|
||||
|
||||
For example, the following will use a JDK client configured with a specific javadoc:java.net.ProxySelector[]:
|
||||
|
||||
include-code::MyConnectorHttpConfiguration[]
|
||||
|
||||
|
||||
|
||||
|
||||
[[io.rest-client.webclient.customization]]
|
||||
=== WebClient Customization
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.docs.io.restclient.webclient.configuration;
|
||||
|
||||
import java.net.ProxySelector;
|
||||
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class MyConnectorHttpConfiguration {
|
||||
|
||||
@Bean
|
||||
ClientHttpConnectorBuilder<?> clientHttpConnectorBuilder(ProxySelector proxySelector) {
|
||||
return ClientHttpConnectorBuilder.jdk().withHttpClientCustomizer((builder) -> builder.proxy(proxySelector));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.springframework.boot.docs.io.restclient.clienthttprequestfactory.configuration
|
||||
|
||||
import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder;
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import java.net.ProxySelector
|
||||
import java.net.http.HttpClient
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class MyConnectorHttpConfiguration {
|
||||
|
||||
@Bean
|
||||
fun clientHttpConnectorBuilder(proxySelector: ProxySelector): ClientHttpConnectorBuilder<*> {
|
||||
return ClientHttpConnectorBuilder.jdk().withHttpClientCustomizer { builder -> builder.proxy(proxySelector) }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -48,6 +48,7 @@ dependencies {
|
||||
exclude(group: "commons-logging", module: "commons-logging")
|
||||
}
|
||||
optional("org.apache.httpcomponents.client5:httpclient5")
|
||||
optional("org.apache.httpcomponents.core5:httpcore5-reactive")
|
||||
optional("org.apache.logging.log4j:log4j-api")
|
||||
optional("org.apache.logging.log4j:log4j-core")
|
||||
optional("org.apache.logging.log4j:log4j-jul")
|
||||
@@ -60,6 +61,7 @@ dependencies {
|
||||
optional("org.crac:crac")
|
||||
optional("org.eclipse.jetty:jetty-alpn-conscrypt-server")
|
||||
optional("org.eclipse.jetty:jetty-client")
|
||||
optional("org.eclipse.jetty:jetty-reactive-httpclient")
|
||||
optional("org.eclipse.jetty:jetty-util")
|
||||
optional("org.eclipse.jetty.ee10:jetty-ee10-servlets")
|
||||
optional("org.eclipse.jetty.ee10:jetty-ee10-webapp")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -35,20 +35,12 @@ import org.springframework.util.Assert;
|
||||
abstract class AbstractClientHttpRequestFactoryBuilder<T extends ClientHttpRequestFactory>
|
||||
implements ClientHttpRequestFactoryBuilder<T> {
|
||||
|
||||
private static final Consumer<?> EMPTY_CUSTOMIZER = (t) -> {
|
||||
};
|
||||
|
||||
private final List<Consumer<T>> customizers;
|
||||
|
||||
protected AbstractClientHttpRequestFactoryBuilder(List<Consumer<T>> customizers) {
|
||||
this.customizers = (customizers != null) ? customizers : Collections.emptyList();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected static <T> Consumer<T> emptyCustomizer() {
|
||||
return (Consumer<T>) EMPTY_CUSTOMIZER;
|
||||
}
|
||||
|
||||
protected final List<Consumer<T>> getCustomizers() {
|
||||
return this.customizers;
|
||||
}
|
||||
@@ -81,4 +73,9 @@ abstract class AbstractClientHttpRequestFactoryBuilder<T extends ClientHttpReque
|
||||
|
||||
protected abstract T createClientHttpRequestFactory(ClientHttpRequestFactorySettings settings);
|
||||
|
||||
protected final HttpClientSettings asHttpClientSettings(ClientHttpRequestFactorySettings settings) {
|
||||
return (settings != null) ? new HttpClientSettings(settings.redirects().httpClientRedirects(),
|
||||
settings.connectTimeout(), settings.readTimeout(), settings.sslBundle()) : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -61,11 +61,21 @@ public record ClientHttpRequestFactorySettings(Redirects redirects, Duration con
|
||||
* @param readTimeout the new read timeout setting
|
||||
* @return a new {@link ClientHttpRequestFactorySettings} instance
|
||||
*/
|
||||
|
||||
public ClientHttpRequestFactorySettings withReadTimeout(Duration readTimeout) {
|
||||
return new ClientHttpRequestFactorySettings(this.redirects, this.connectTimeout, readTimeout, this.sslBundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpRequestFactorySettings} instance with an updated
|
||||
* connect and read timeout setting.
|
||||
* @param connectTimeout the new connect timeout setting
|
||||
* @param readTimeout the new read timeout setting
|
||||
* @return a new {@link ClientHttpRequestFactorySettings} instance
|
||||
*/
|
||||
public ClientHttpRequestFactorySettings withTimeouts(Duration connectTimeout, Duration readTimeout) {
|
||||
return new ClientHttpRequestFactorySettings(this.redirects, connectTimeout, readTimeout, this.sslBundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpRequestFactorySettings} instance with an updated SSL
|
||||
* bundle setting.
|
||||
@@ -113,17 +123,27 @@ public record ClientHttpRequestFactorySettings(Redirects redirects, Duration con
|
||||
/**
|
||||
* Follow redirects (if the underlying library has support).
|
||||
*/
|
||||
FOLLOW_WHEN_POSSIBLE,
|
||||
FOLLOW_WHEN_POSSIBLE(HttpRedirects.FOLLOW_WHEN_POSSIBLE),
|
||||
|
||||
/**
|
||||
* Follow redirects (fail if the underlying library has no support).
|
||||
*/
|
||||
FOLLOW,
|
||||
FOLLOW(HttpRedirects.FOLLOW),
|
||||
|
||||
/**
|
||||
* Don't follow redirects (fail if the underlying library has no support).
|
||||
*/
|
||||
DONT_FOLLOW
|
||||
DONT_FOLLOW(HttpRedirects.DONT_FOLLOW);
|
||||
|
||||
private final HttpRedirects httpClientRedirects;
|
||||
|
||||
Redirects(HttpRedirects httpClientRedirects) {
|
||||
this.httpClientRedirects = httpClientRedirects;
|
||||
}
|
||||
|
||||
HttpRedirects httpClientRedirects() {
|
||||
return this.httpClientRedirects;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -14,18 +14,26 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.web.reactive.function.client;
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Tests for {@link JdkClientHttpConnectorFactory}.
|
||||
* Helper for empty functional interfaces.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class JdkClientHttpConnectorFactoryTests extends AbstractClientHttpConnectorFactoryTests {
|
||||
final class Empty {
|
||||
|
||||
@Override
|
||||
protected ClientHttpConnectorFactory<?> getFactory() {
|
||||
return new JdkClientHttpConnectorFactory();
|
||||
private static final Consumer<?> EMPTY_CUSTOMIZER = (t) -> {
|
||||
};
|
||||
|
||||
private Empty() {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T> Consumer<T> consumer() {
|
||||
return (Consumer<T>) EMPTY_CUSTOMIZER;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.http.client;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
|
||||
/**
|
||||
* Settings that can be applied when creating a blocking or reactive HTTP client.
|
||||
*
|
||||
* @param redirects the follow redirect strategy to use or null to redirect whenever the
|
||||
* underlying library allows it
|
||||
* @param connectTimeout the connect timeout
|
||||
* @param readTimeout the read timeout
|
||||
* @param sslBundle the SSL bundle providing SSL configuration
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public record HttpClientSettings(HttpRedirects redirects, Duration connectTimeout, Duration readTimeout,
|
||||
SslBundle sslBundle) {
|
||||
|
||||
static final HttpClientSettings DEFAULTS = new HttpClientSettings(null, null, null, null);
|
||||
|
||||
public HttpClientSettings {
|
||||
redirects = (redirects != null) ? redirects : HttpRedirects.FOLLOW_WHEN_POSSIBLE;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -16,34 +16,21 @@
|
||||
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.hc.client5.http.classic.HttpClient;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
|
||||
import org.apache.hc.client5.http.protocol.RedirectStrategy;
|
||||
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
|
||||
import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
|
||||
import org.apache.hc.client5.http.ssl.TlsSocketStrategy;
|
||||
import org.apache.hc.core5.http.HttpRequest;
|
||||
import org.apache.hc.core5.http.HttpResponse;
|
||||
import org.apache.hc.core5.http.io.SocketConfig;
|
||||
import org.apache.hc.core5.http.protocol.HttpContext;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
@@ -59,56 +46,30 @@ import org.springframework.util.ClassUtils;
|
||||
public final class HttpComponentsClientHttpRequestFactoryBuilder
|
||||
extends AbstractClientHttpRequestFactoryBuilder<HttpComponentsClientHttpRequestFactory> {
|
||||
|
||||
private final Consumer<HttpClientBuilder> httpClientCustomizer;
|
||||
|
||||
private final Consumer<PoolingHttpClientConnectionManagerBuilder> connectionManagerCustomizer;
|
||||
|
||||
private final Consumer<SocketConfig.Builder> socketConfigCustomizer;
|
||||
|
||||
private final Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer;
|
||||
|
||||
private final Function<SslBundle, TlsSocketStrategy> tlsSocketStrategyFactory;
|
||||
private final HttpComponentsHttpClientBuilder httpClientBuilder;
|
||||
|
||||
HttpComponentsClientHttpRequestFactoryBuilder() {
|
||||
this(Collections.emptyList(), emptyCustomizer(), emptyCustomizer(), emptyCustomizer(), emptyCustomizer(),
|
||||
HttpComponentsClientHttpRequestFactoryBuilder::createTlsSocketStrategy);
|
||||
}
|
||||
|
||||
private static TlsSocketStrategy createTlsSocketStrategy(SslBundle sslBundle) {
|
||||
SslOptions options = sslBundle.getOptions();
|
||||
return new DefaultClientTlsStrategy(sslBundle.createSslContext(), options.getEnabledProtocols(),
|
||||
options.getCiphers(), null, new DefaultHostnameVerifier());
|
||||
this(null, new HttpComponentsHttpClientBuilder());
|
||||
}
|
||||
|
||||
private HttpComponentsClientHttpRequestFactoryBuilder(
|
||||
List<Consumer<HttpComponentsClientHttpRequestFactory>> customizers,
|
||||
Consumer<HttpClientBuilder> httpClientCustomizer,
|
||||
Consumer<PoolingHttpClientConnectionManagerBuilder> connectionManagerCustomizer,
|
||||
Consumer<SocketConfig.Builder> socketConfigCustomizer,
|
||||
Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer,
|
||||
Function<SslBundle, TlsSocketStrategy> tlsSocketStrategyFactory) {
|
||||
HttpComponentsHttpClientBuilder httpClientBuilder) {
|
||||
super(customizers);
|
||||
this.httpClientCustomizer = httpClientCustomizer;
|
||||
this.connectionManagerCustomizer = connectionManagerCustomizer;
|
||||
this.socketConfigCustomizer = socketConfigCustomizer;
|
||||
this.defaultRequestConfigCustomizer = defaultRequestConfigCustomizer;
|
||||
this.tlsSocketStrategyFactory = tlsSocketStrategyFactory;
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpComponentsClientHttpRequestFactoryBuilder withCustomizer(
|
||||
Consumer<HttpComponentsClientHttpRequestFactory> customizer) {
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder(mergedCustomizers(customizer),
|
||||
this.httpClientCustomizer, this.connectionManagerCustomizer, this.socketConfigCustomizer,
|
||||
this.defaultRequestConfigCustomizer, this.tlsSocketStrategyFactory);
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder(mergedCustomizers(customizer), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpComponentsClientHttpRequestFactoryBuilder withCustomizers(
|
||||
Collection<Consumer<HttpComponentsClientHttpRequestFactory>> customizers) {
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder(mergedCustomizers(customizers),
|
||||
this.httpClientCustomizer, this.connectionManagerCustomizer, this.socketConfigCustomizer,
|
||||
this.defaultRequestConfigCustomizer, this.tlsSocketStrategyFactory);
|
||||
this.httpClientBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,8 +82,7 @@ public final class HttpComponentsClientHttpRequestFactoryBuilder
|
||||
Consumer<HttpClientBuilder> httpClientCustomizer) {
|
||||
Assert.notNull(httpClientCustomizer, "'httpClientCustomizer' must not be null");
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientCustomizer.andThen(httpClientCustomizer), this.connectionManagerCustomizer,
|
||||
this.socketConfigCustomizer, this.defaultRequestConfigCustomizer, this.tlsSocketStrategyFactory);
|
||||
this.httpClientBuilder.withCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,9 +95,8 @@ public final class HttpComponentsClientHttpRequestFactoryBuilder
|
||||
public HttpComponentsClientHttpRequestFactoryBuilder withConnectionManagerCustomizer(
|
||||
Consumer<PoolingHttpClientConnectionManagerBuilder> connectionManagerCustomizer) {
|
||||
Assert.notNull(connectionManagerCustomizer, "'connectionManagerCustomizer' must not be null");
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(), this.httpClientCustomizer,
|
||||
this.connectionManagerCustomizer.andThen(connectionManagerCustomizer), this.socketConfigCustomizer,
|
||||
this.defaultRequestConfigCustomizer, this.tlsSocketStrategyFactory);
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withConnectionManagerCustomizer(connectionManagerCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,9 +109,8 @@ public final class HttpComponentsClientHttpRequestFactoryBuilder
|
||||
public HttpComponentsClientHttpRequestFactoryBuilder withSocketConfigCustomizer(
|
||||
Consumer<SocketConfig.Builder> socketConfigCustomizer) {
|
||||
Assert.notNull(socketConfigCustomizer, "'socketConfigCustomizer' must not be null");
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(), this.httpClientCustomizer,
|
||||
this.connectionManagerCustomizer, this.socketConfigCustomizer.andThen(socketConfigCustomizer),
|
||||
this.defaultRequestConfigCustomizer, this.tlsSocketStrategyFactory);
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withSocketConfigCustomizer(socketConfigCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,9 +123,8 @@ public final class HttpComponentsClientHttpRequestFactoryBuilder
|
||||
public HttpComponentsClientHttpRequestFactoryBuilder withTlsSocketStrategyFactory(
|
||||
Function<SslBundle, TlsSocketStrategy> tlsSocketStrategyFactory) {
|
||||
Assert.notNull(tlsSocketStrategyFactory, "'tlsSocketStrategyFactory' must not be null");
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(), this.httpClientCustomizer,
|
||||
this.connectionManagerCustomizer, this.socketConfigCustomizer, this.defaultRequestConfigCustomizer,
|
||||
tlsSocketStrategyFactory);
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withTlsSocketStrategyFactory(tlsSocketStrategyFactory));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,87 +138,20 @@ public final class HttpComponentsClientHttpRequestFactoryBuilder
|
||||
public HttpComponentsClientHttpRequestFactoryBuilder withDefaultRequestConfigCustomizer(
|
||||
Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer) {
|
||||
Assert.notNull(defaultRequestConfigCustomizer, "'defaultRequestConfigCustomizer' must not be null");
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(), this.httpClientCustomizer,
|
||||
this.connectionManagerCustomizer, this.socketConfigCustomizer,
|
||||
this.defaultRequestConfigCustomizer.andThen(defaultRequestConfigCustomizer),
|
||||
this.tlsSocketStrategyFactory);
|
||||
return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withDefaultRequestConfigCustomizer(defaultRequestConfigCustomizer));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpComponentsClientHttpRequestFactory createClientHttpRequestFactory(
|
||||
ClientHttpRequestFactorySettings settings) {
|
||||
HttpClient httpClient = createHttpClient(settings);
|
||||
HttpClient httpClient = this.httpClientBuilder.build(asHttpClientSettings(settings.withConnectTimeout(null)));
|
||||
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(settings::connectTimeout).asInt(Duration::toMillis).to(factory::setConnectTimeout);
|
||||
return factory;
|
||||
}
|
||||
|
||||
private HttpClient createHttpClient(ClientHttpRequestFactorySettings settings) {
|
||||
HttpClientBuilder builder = HttpClientBuilder.create()
|
||||
.useSystemProperties()
|
||||
.setRedirectStrategy(asRedirectStrategy(settings.redirects()))
|
||||
.setConnectionManager(createConnectionManager(settings))
|
||||
.setDefaultRequestConfig(createDefaultRequestConfig());
|
||||
this.httpClientCustomizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private RedirectStrategy asRedirectStrategy(Redirects redirects) {
|
||||
return switch (redirects) {
|
||||
case FOLLOW_WHEN_POSSIBLE, FOLLOW -> DefaultRedirectStrategy.INSTANCE;
|
||||
case DONT_FOLLOW -> NoFollowRedirectStrategy.INSTANCE;
|
||||
};
|
||||
}
|
||||
|
||||
private PoolingHttpClientConnectionManager createConnectionManager(ClientHttpRequestFactorySettings settings) {
|
||||
PoolingHttpClientConnectionManagerBuilder builder = PoolingHttpClientConnectionManagerBuilder.create()
|
||||
.useSystemProperties();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
builder.setDefaultSocketConfig(createSocketConfig(settings));
|
||||
map.from(settings::sslBundle).as(this.tlsSocketStrategyFactory).to(builder::setTlsSocketStrategy);
|
||||
this.connectionManagerCustomizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private SocketConfig createSocketConfig(ClientHttpRequestFactorySettings settings) {
|
||||
SocketConfig.Builder builder = SocketConfig.custom();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(settings::readTimeout)
|
||||
.asInt(Duration::toMillis)
|
||||
.to((timeout) -> builder.setSoTimeout(timeout, TimeUnit.MILLISECONDS));
|
||||
this.socketConfigCustomizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private RequestConfig createDefaultRequestConfig() {
|
||||
RequestConfig.Builder builder = RequestConfig.custom();
|
||||
this.defaultRequestConfigCustomizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link RedirectStrategy} that never follows redirects.
|
||||
*/
|
||||
private static final class NoFollowRedirectStrategy implements RedirectStrategy {
|
||||
|
||||
private static final RedirectStrategy INSTANCE = new NoFollowRedirectStrategy();
|
||||
|
||||
private NoFollowRedirectStrategy() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getLocationURI(HttpRequest request, HttpResponse response, HttpContext context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class Classes {
|
||||
|
||||
static final String HTTP_CLIENTS = "org.apache.hc.client5.http.impl.classic.HttpClients";
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* 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.http.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.hc.client5.http.async.HttpAsyncClient;
|
||||
import org.apache.hc.client5.http.config.ConnectionConfig;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
|
||||
import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
|
||||
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
|
||||
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
|
||||
import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Builder that can be used to create a
|
||||
* <a href="https://hc.apache.org/httpcomponents-client-ga/">Apache HttpComponents</a>
|
||||
* {@link HttpAsyncClient}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public final class HttpComponentsHttpAsyncClientBuilder {
|
||||
|
||||
private final Consumer<HttpAsyncClientBuilder> customizer;
|
||||
|
||||
private final Consumer<PoolingAsyncClientConnectionManagerBuilder> connectionManagerCustomizer;
|
||||
|
||||
private final Consumer<ConnectionConfig.Builder> connectionConfigCustomizer;
|
||||
|
||||
private final Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer;
|
||||
|
||||
private final Function<SslBundle, TlsStrategy> tlsStrategyFactory;
|
||||
|
||||
public HttpComponentsHttpAsyncClientBuilder() {
|
||||
this(Empty.consumer(), Empty.consumer(), Empty.consumer(), Empty.consumer(),
|
||||
HttpComponentsSslBundleTlsStrategy::get);
|
||||
}
|
||||
|
||||
private HttpComponentsHttpAsyncClientBuilder(Consumer<HttpAsyncClientBuilder> customizer,
|
||||
Consumer<PoolingAsyncClientConnectionManagerBuilder> connectionManagerCustomizer,
|
||||
Consumer<ConnectionConfig.Builder> connectionConfigCustomizer,
|
||||
Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer,
|
||||
Function<SslBundle, TlsStrategy> tlsStrategyFactory) {
|
||||
this.customizer = customizer;
|
||||
this.connectionManagerCustomizer = connectionManagerCustomizer;
|
||||
this.connectionConfigCustomizer = connectionConfigCustomizer;
|
||||
this.defaultRequestConfigCustomizer = defaultRequestConfigCustomizer;
|
||||
this.tlsStrategyFactory = tlsStrategyFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpAsyncClientBuilder} that applies additional
|
||||
* customization to the underlying {@link HttpAsyncClientBuilder}.
|
||||
* @param customizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsHttpAsyncClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsHttpAsyncClientBuilder withCustomizer(Consumer<HttpAsyncClientBuilder> customizer) {
|
||||
Assert.notNull(customizer, "'customizer' must not be null");
|
||||
return new HttpComponentsHttpAsyncClientBuilder(this.customizer.andThen(customizer),
|
||||
this.connectionManagerCustomizer, this.connectionConfigCustomizer, this.defaultRequestConfigCustomizer,
|
||||
this.tlsStrategyFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpAsyncClientBuilder} that applies additional
|
||||
* customization to the underlying {@link PoolingAsyncClientConnectionManagerBuilder}.
|
||||
* @param connectionManagerCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsHttpAsyncClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsHttpAsyncClientBuilder withConnectionManagerCustomizer(
|
||||
Consumer<PoolingAsyncClientConnectionManagerBuilder> connectionManagerCustomizer) {
|
||||
Assert.notNull(connectionManagerCustomizer, "'connectionManagerCustomizer' must not be null");
|
||||
return new HttpComponentsHttpAsyncClientBuilder(this.customizer,
|
||||
this.connectionManagerCustomizer.andThen(connectionManagerCustomizer), this.connectionConfigCustomizer,
|
||||
this.defaultRequestConfigCustomizer, this.tlsStrategyFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpAsyncClientBuilder} that applies additional
|
||||
* customization to the underlying
|
||||
* {@link org.apache.hc.client5.http.config.ConnectionConfig.Builder}.
|
||||
* @param connectionConfigCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsHttpAsyncClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsHttpAsyncClientBuilder withConnectionConfigCustomizer(
|
||||
Consumer<ConnectionConfig.Builder> connectionConfigCustomizer) {
|
||||
Assert.notNull(connectionConfigCustomizer, "'connectionConfigCustomizer' must not be null");
|
||||
return new HttpComponentsHttpAsyncClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.connectionConfigCustomizer.andThen(connectionConfigCustomizer),
|
||||
this.defaultRequestConfigCustomizer, this.tlsStrategyFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpAsyncClientBuilder} with a replacement
|
||||
* {@link TlsStrategy} factory.
|
||||
* @param tlsStrategyFactory the new factory used to create a {@link TlsStrategy} for
|
||||
* a given {@link SslBundle}
|
||||
* @return a new {@link HttpComponentsHttpAsyncClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsHttpAsyncClientBuilder withTlsStrategyFactory(
|
||||
Function<SslBundle, TlsStrategy> tlsStrategyFactory) {
|
||||
Assert.notNull(tlsStrategyFactory, "'tlsStrategyFactory' must not be null");
|
||||
return new HttpComponentsHttpAsyncClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.connectionConfigCustomizer, this.defaultRequestConfigCustomizer, tlsStrategyFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpAsyncClientBuilder} that applies additional
|
||||
* customization to the underlying
|
||||
* {@link org.apache.hc.client5.http.config.RequestConfig.Builder} used for default
|
||||
* requests.
|
||||
* @param defaultRequestConfigCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsHttpAsyncClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsHttpAsyncClientBuilder withDefaultRequestConfigCustomizer(
|
||||
Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer) {
|
||||
Assert.notNull(defaultRequestConfigCustomizer, "'defaultRequestConfigCustomizer' must not be null");
|
||||
return new HttpComponentsHttpAsyncClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.connectionConfigCustomizer,
|
||||
this.defaultRequestConfigCustomizer.andThen(defaultRequestConfigCustomizer), this.tlsStrategyFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new {@link HttpAsyncClient} instance with the given settings applied.
|
||||
* @param settings the settings to apply
|
||||
* @return a new {@link CloseableHttpAsyncClient} instance
|
||||
*/
|
||||
public CloseableHttpAsyncClient build(HttpClientSettings settings) {
|
||||
settings = (settings != null) ? settings : HttpClientSettings.DEFAULTS;
|
||||
HttpAsyncClientBuilder builder = HttpAsyncClientBuilder.create()
|
||||
.useSystemProperties()
|
||||
.setRedirectStrategy(HttpComponentsRedirectStrategy.get(settings.redirects()))
|
||||
.setConnectionManager(createConnectionManager(settings))
|
||||
.setDefaultRequestConfig(createDefaultRequestConfig());
|
||||
this.customizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private PoolingAsyncClientConnectionManager createConnectionManager(HttpClientSettings settings) {
|
||||
PoolingAsyncClientConnectionManagerBuilder builder = PoolingAsyncClientConnectionManagerBuilder.create()
|
||||
.useSystemProperties();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
builder.setDefaultConnectionConfig(createConnectionConfig(settings));
|
||||
map.from(settings::sslBundle).as(this.tlsStrategyFactory).to(builder::setTlsStrategy);
|
||||
this.connectionManagerCustomizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private ConnectionConfig createConnectionConfig(HttpClientSettings settings) {
|
||||
ConnectionConfig.Builder builder = ConnectionConfig.custom();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(settings::connectTimeout)
|
||||
.as(Duration::toMillis)
|
||||
.to((timeout) -> builder.setConnectTimeout(timeout, TimeUnit.MILLISECONDS));
|
||||
map.from(settings::readTimeout)
|
||||
.asInt(Duration::toMillis)
|
||||
.to((timeout) -> builder.setSocketTimeout(timeout, TimeUnit.MILLISECONDS));
|
||||
this.connectionConfigCustomizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private RequestConfig createDefaultRequestConfig() {
|
||||
RequestConfig.Builder builder = RequestConfig.custom();
|
||||
this.defaultRequestConfigCustomizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* 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.http.client;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
|
||||
import org.apache.hc.client5.http.ssl.TlsSocketStrategy;
|
||||
import org.apache.hc.core5.http.io.SocketConfig;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Builder that can be used to create a
|
||||
* <a href="https://hc.apache.org/httpcomponents-client-ga/">Apache HttpComponents</a>
|
||||
* {@link HttpClient}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public final class HttpComponentsHttpClientBuilder {
|
||||
|
||||
private final Consumer<HttpClientBuilder> customizer;
|
||||
|
||||
private final Consumer<PoolingHttpClientConnectionManagerBuilder> connectionManagerCustomizer;
|
||||
|
||||
private final Consumer<SocketConfig.Builder> socketConfigCustomizer;
|
||||
|
||||
private final Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer;
|
||||
|
||||
private final Function<SslBundle, TlsSocketStrategy> tlsSocketStrategyFactory;
|
||||
|
||||
public HttpComponentsHttpClientBuilder() {
|
||||
this(Empty.consumer(), Empty.consumer(), Empty.consumer(), Empty.consumer(),
|
||||
HttpComponentsSslBundleTlsStrategy::get);
|
||||
}
|
||||
|
||||
private HttpComponentsHttpClientBuilder(Consumer<HttpClientBuilder> customizer,
|
||||
Consumer<PoolingHttpClientConnectionManagerBuilder> connectionManagerCustomizer,
|
||||
Consumer<SocketConfig.Builder> socketConfigCustomizer,
|
||||
Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer,
|
||||
Function<SslBundle, TlsSocketStrategy> tlsSocketStrategyFactory) {
|
||||
this.customizer = customizer;
|
||||
this.connectionManagerCustomizer = connectionManagerCustomizer;
|
||||
this.socketConfigCustomizer = socketConfigCustomizer;
|
||||
this.defaultRequestConfigCustomizer = defaultRequestConfigCustomizer;
|
||||
this.tlsSocketStrategyFactory = tlsSocketStrategyFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpClientBuilder} that applies additional
|
||||
* customization to the underlying {@link HttpClientBuilder}.
|
||||
* @param customizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsHttpClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsHttpClientBuilder withCustomizer(Consumer<HttpClientBuilder> customizer) {
|
||||
Assert.notNull(customizer, "'customizer' must not be null");
|
||||
return new HttpComponentsHttpClientBuilder(this.customizer.andThen(customizer),
|
||||
this.connectionManagerCustomizer, this.socketConfigCustomizer, this.defaultRequestConfigCustomizer,
|
||||
this.tlsSocketStrategyFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpClientBuilder} that applies additional
|
||||
* customization to the underlying {@link PoolingHttpClientConnectionManagerBuilder}.
|
||||
* @param connectionManagerCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsHttpClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsHttpClientBuilder withConnectionManagerCustomizer(
|
||||
Consumer<PoolingHttpClientConnectionManagerBuilder> connectionManagerCustomizer) {
|
||||
Assert.notNull(connectionManagerCustomizer, "'connectionManagerCustomizer' must not be null");
|
||||
return new HttpComponentsHttpClientBuilder(this.customizer,
|
||||
this.connectionManagerCustomizer.andThen(connectionManagerCustomizer), this.socketConfigCustomizer,
|
||||
this.defaultRequestConfigCustomizer, this.tlsSocketStrategyFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpClientBuilder} that applies additional
|
||||
* customization to the underlying
|
||||
* {@link org.apache.hc.core5.http.io.SocketConfig.Builder}.
|
||||
* @param socketConfigCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsHttpClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsHttpClientBuilder withSocketConfigCustomizer(
|
||||
Consumer<SocketConfig.Builder> socketConfigCustomizer) {
|
||||
Assert.notNull(socketConfigCustomizer, "'socketConfigCustomizer' must not be null");
|
||||
return new HttpComponentsHttpClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.socketConfigCustomizer.andThen(socketConfigCustomizer), this.defaultRequestConfigCustomizer,
|
||||
this.tlsSocketStrategyFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpClientBuilder} with a replacement
|
||||
* {@link TlsSocketStrategy} factory.
|
||||
* @param tlsSocketStrategyFactory the new factory used to create a
|
||||
* {@link TlsSocketStrategy} for a given {@link SslBundle}
|
||||
* @return a new {@link HttpComponentsHttpClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsHttpClientBuilder withTlsSocketStrategyFactory(
|
||||
Function<SslBundle, TlsSocketStrategy> tlsSocketStrategyFactory) {
|
||||
Assert.notNull(tlsSocketStrategyFactory, "'tlsSocketStrategyFactory' must not be null");
|
||||
return new HttpComponentsHttpClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.socketConfigCustomizer, this.defaultRequestConfigCustomizer, tlsSocketStrategyFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsHttpClientBuilder} that applies additional
|
||||
* customization to the underlying
|
||||
* {@link org.apache.hc.client5.http.config.RequestConfig.Builder} used for default
|
||||
* requests.
|
||||
* @param defaultRequestConfigCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsHttpClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsHttpClientBuilder withDefaultRequestConfigCustomizer(
|
||||
Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer) {
|
||||
Assert.notNull(defaultRequestConfigCustomizer, "'defaultRequestConfigCustomizer' must not be null");
|
||||
return new HttpComponentsHttpClientBuilder(this.customizer, this.connectionManagerCustomizer,
|
||||
this.socketConfigCustomizer,
|
||||
this.defaultRequestConfigCustomizer.andThen(defaultRequestConfigCustomizer),
|
||||
this.tlsSocketStrategyFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new {@link HttpClient} instance with the given settings applied.
|
||||
* @param settings the settings to apply
|
||||
* @return a new {@link HttpClient} instance
|
||||
*/
|
||||
public CloseableHttpClient build(HttpClientSettings settings) {
|
||||
settings = (settings != null) ? settings : HttpClientSettings.DEFAULTS;
|
||||
Assert.isTrue(settings.connectTimeout() == null, "'settings' must not have a 'connectTimeout'");
|
||||
HttpClientBuilder builder = HttpClientBuilder.create()
|
||||
.useSystemProperties()
|
||||
.setRedirectStrategy(HttpComponentsRedirectStrategy.get(settings.redirects()))
|
||||
.setConnectionManager(createConnectionManager(settings))
|
||||
.setDefaultRequestConfig(createDefaultRequestConfig());
|
||||
this.customizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private PoolingHttpClientConnectionManager createConnectionManager(HttpClientSettings settings) {
|
||||
PoolingHttpClientConnectionManagerBuilder builder = PoolingHttpClientConnectionManagerBuilder.create()
|
||||
.useSystemProperties();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
builder.setDefaultSocketConfig(createSocketConfig(settings));
|
||||
map.from(settings::sslBundle).as(this.tlsSocketStrategyFactory).to(builder::setTlsSocketStrategy);
|
||||
this.connectionManagerCustomizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private SocketConfig createSocketConfig(HttpClientSettings settings) {
|
||||
SocketConfig.Builder builder = SocketConfig.custom();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(settings::readTimeout)
|
||||
.asInt(Duration::toMillis)
|
||||
.to((timeout) -> builder.setSoTimeout(timeout, TimeUnit.MILLISECONDS));
|
||||
this.socketConfigCustomizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private RequestConfig createDefaultRequestConfig() {
|
||||
RequestConfig.Builder builder = RequestConfig.custom();
|
||||
this.defaultRequestConfigCustomizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.http.client;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
|
||||
import org.apache.hc.client5.http.protocol.RedirectStrategy;
|
||||
import org.apache.hc.core5.http.HttpRequest;
|
||||
import org.apache.hc.core5.http.HttpResponse;
|
||||
import org.apache.hc.core5.http.protocol.HttpContext;
|
||||
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
|
||||
|
||||
/**
|
||||
* Adapts {@link Redirects} to an
|
||||
* <a href="https://hc.apache.org/httpcomponents-client-ga/">Apache HttpComponents</a>
|
||||
* {@link RedirectStrategy}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class HttpComponentsRedirectStrategy {
|
||||
|
||||
private HttpComponentsRedirectStrategy() {
|
||||
}
|
||||
|
||||
static RedirectStrategy get(HttpRedirects redirects) {
|
||||
return switch (redirects) {
|
||||
case FOLLOW_WHEN_POSSIBLE, FOLLOW -> DefaultRedirectStrategy.INSTANCE;
|
||||
case DONT_FOLLOW -> NoFollowRedirectStrategy.INSTANCE;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link RedirectStrategy} that never follows redirects.
|
||||
*/
|
||||
private static final class NoFollowRedirectStrategy implements RedirectStrategy {
|
||||
|
||||
private static final RedirectStrategy INSTANCE = new NoFollowRedirectStrategy();
|
||||
|
||||
private NoFollowRedirectStrategy() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getLocationURI(HttpRequest request, HttpResponse response, HttpContext context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.http.client;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
|
||||
import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
|
||||
/**
|
||||
* Adapts {@link SslBundle} to an
|
||||
* <a href="https://hc.apache.org/httpcomponents-client-ga/">Apache HttpComponents</a>
|
||||
* {@link DefaultClientTlsStrategy}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class HttpComponentsSslBundleTlsStrategy {
|
||||
|
||||
private HttpComponentsSslBundleTlsStrategy() {
|
||||
}
|
||||
|
||||
static DefaultClientTlsStrategy get(SslBundle sslBundle) {
|
||||
SslOptions options = sslBundle.getOptions();
|
||||
SSLContext sslContext = sslBundle.createSslContext();
|
||||
return new DefaultClientTlsStrategy(sslContext, options.getEnabledProtocols(), options.getCiphers(), null,
|
||||
new DefaultHostnameVerifier());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -14,18 +14,29 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.web.reactive.function.client;
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpComponentsClientHttpConnectorFactory}.
|
||||
* Redirect strategies support by HTTP clients.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
*/
|
||||
class HttpComponentsClientHttpConnectorFactoryTests extends AbstractClientHttpConnectorFactoryTests {
|
||||
public enum HttpRedirects {
|
||||
|
||||
@Override
|
||||
protected ClientHttpConnectorFactory<?> getFactory() {
|
||||
return new HttpComponentsClientHttpConnectorFactory();
|
||||
}
|
||||
/**
|
||||
* Follow redirects (if the underlying library has support).
|
||||
*/
|
||||
FOLLOW_WHEN_POSSIBLE,
|
||||
|
||||
/**
|
||||
* Follow redirects (fail if the underlying library has no support).
|
||||
*/
|
||||
FOLLOW,
|
||||
|
||||
/**
|
||||
* Don't follow redirects (fail if the underlying library has no support).
|
||||
*/
|
||||
DONT_FOLLOW
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -17,17 +17,11 @@
|
||||
package org.springframework.boot.http.client;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpClient.Redirect;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.net.ssl.SSLParameters;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.http.client.JdkClientHttpRequestFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
@@ -40,30 +34,30 @@ import org.springframework.util.ClassUtils;
|
||||
* @author Scott Frederick
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public class JdkClientHttpRequestFactoryBuilder
|
||||
public final class JdkClientHttpRequestFactoryBuilder
|
||||
extends AbstractClientHttpRequestFactoryBuilder<JdkClientHttpRequestFactory> {
|
||||
|
||||
private final Consumer<HttpClient.Builder> httpClientCustomizer;
|
||||
private final JdkHttpClientBuilder httpClientBuilder;
|
||||
|
||||
JdkClientHttpRequestFactoryBuilder() {
|
||||
this(null, emptyCustomizer());
|
||||
this(null, new JdkHttpClientBuilder());
|
||||
}
|
||||
|
||||
private JdkClientHttpRequestFactoryBuilder(List<Consumer<JdkClientHttpRequestFactory>> customizers,
|
||||
Consumer<HttpClient.Builder> httpClientCustomizer) {
|
||||
JdkHttpClientBuilder httpClientBuilder) {
|
||||
super(customizers);
|
||||
this.httpClientCustomizer = httpClientCustomizer;
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JdkClientHttpRequestFactoryBuilder withCustomizer(Consumer<JdkClientHttpRequestFactory> customizer) {
|
||||
return new JdkClientHttpRequestFactoryBuilder(mergedCustomizers(customizer), this.httpClientCustomizer);
|
||||
return new JdkClientHttpRequestFactoryBuilder(mergedCustomizers(customizer), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JdkClientHttpRequestFactoryBuilder withCustomizers(
|
||||
Collection<Consumer<JdkClientHttpRequestFactory>> customizers) {
|
||||
return new JdkClientHttpRequestFactoryBuilder(mergedCustomizers(customizers), this.httpClientCustomizer);
|
||||
return new JdkClientHttpRequestFactoryBuilder(mergedCustomizers(customizers), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,44 +70,18 @@ public class JdkClientHttpRequestFactoryBuilder
|
||||
Consumer<HttpClient.Builder> httpClientCustomizer) {
|
||||
Assert.notNull(httpClientCustomizer, "'httpClientCustomizer' must not be null");
|
||||
return new JdkClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientCustomizer.andThen(httpClientCustomizer));
|
||||
this.httpClientBuilder.withCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JdkClientHttpRequestFactory createClientHttpRequestFactory(ClientHttpRequestFactorySettings settings) {
|
||||
HttpClient httpClient = createHttpClient(settings);
|
||||
HttpClient httpClient = this.httpClientBuilder.build(asHttpClientSettings(settings.withReadTimeout(null)));
|
||||
JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(httpClient);
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(settings::readTimeout).to(requestFactory::setReadTimeout);
|
||||
return requestFactory;
|
||||
}
|
||||
|
||||
private HttpClient createHttpClient(ClientHttpRequestFactorySettings settings) {
|
||||
HttpClient.Builder builder = HttpClient.newBuilder();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(settings::connectTimeout).to(builder::connectTimeout);
|
||||
map.from(settings::sslBundle).as(SslBundle::createSslContext).to(builder::sslContext);
|
||||
map.from(settings::sslBundle).as(this::asSslParameters).to(builder::sslParameters);
|
||||
map.from(settings::redirects).as(this::asHttpClientRedirect).to(builder::followRedirects);
|
||||
this.httpClientCustomizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private SSLParameters asSslParameters(SslBundle sslBundle) {
|
||||
SslOptions options = sslBundle.getOptions();
|
||||
SSLParameters parameters = new SSLParameters();
|
||||
parameters.setCipherSuites(options.getCiphers());
|
||||
parameters.setProtocols(options.getEnabledProtocols());
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private Redirect asHttpClientRedirect(Redirects redirects) {
|
||||
return switch (redirects) {
|
||||
case FOLLOW_WHEN_POSSIBLE, FOLLOW -> Redirect.NORMAL;
|
||||
case DONT_FOLLOW -> Redirect.NEVER;
|
||||
};
|
||||
}
|
||||
|
||||
static class Classes {
|
||||
|
||||
static final String HTTP_CLIENT = "java.net.http.HttpClient";
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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.http.client;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpClient.Redirect;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.net.ssl.SSLParameters;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Builder that can be used to create a JDK {@link HttpClient}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public final class JdkHttpClientBuilder {
|
||||
|
||||
private final Consumer<HttpClient.Builder> customizer;
|
||||
|
||||
public JdkHttpClientBuilder() {
|
||||
this(Empty.consumer());
|
||||
}
|
||||
|
||||
private JdkHttpClientBuilder(Consumer<HttpClient.Builder> customizer) {
|
||||
this.customizer = customizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JdkHttpClientBuilder} that applies additional customization to
|
||||
* the underlying {@link java.net.http.HttpClient.Builder}.
|
||||
* @param customizer the customizer to apply
|
||||
* @return a new {@link JdkHttpClientBuilder} instance
|
||||
*/
|
||||
public JdkHttpClientBuilder withCustomizer(Consumer<HttpClient.Builder> customizer) {
|
||||
Assert.notNull(customizer, "'customizer' must not be null");
|
||||
return new JdkHttpClientBuilder(this.customizer.andThen(customizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new {@link HttpClient} instance with the given settings applied.
|
||||
* @param settings the settings to apply
|
||||
* @return a new {@link HttpClient} instance
|
||||
*/
|
||||
public HttpClient build(HttpClientSettings settings) {
|
||||
settings = (settings != null) ? settings : HttpClientSettings.DEFAULTS;
|
||||
Assert.isTrue(settings.readTimeout() == null, "'settings' must not have a 'readTimeout'");
|
||||
HttpClient.Builder builder = HttpClient.newBuilder();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(settings::redirects).as(this::asHttpClientRedirect).to(builder::followRedirects);
|
||||
map.from(settings::connectTimeout).to(builder::connectTimeout);
|
||||
map.from(settings::sslBundle).as(SslBundle::createSslContext).to(builder::sslContext);
|
||||
map.from(settings::sslBundle).as(this::asSslParameters).to(builder::sslParameters);
|
||||
this.customizer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private SSLParameters asSslParameters(SslBundle sslBundle) {
|
||||
SslOptions options = sslBundle.getOptions();
|
||||
SSLParameters parameters = new SSLParameters();
|
||||
parameters.setCipherSuites(options.getCiphers());
|
||||
parameters.setProtocols(options.getEnabledProtocols());
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private Redirect asHttpClientRedirect(HttpRedirects redirects) {
|
||||
return switch (redirects) {
|
||||
case FOLLOW_WHEN_POSSIBLE, FOLLOW -> Redirect.NORMAL;
|
||||
case DONT_FOLLOW -> Redirect.NEVER;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -21,19 +21,11 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpClientTransport;
|
||||
import org.eclipse.jetty.client.transport.HttpClientTransportDynamic;
|
||||
import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP;
|
||||
import org.eclipse.jetty.io.ClientConnector;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.http.client.JettyClientHttpRequestFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
@@ -49,36 +41,27 @@ import org.springframework.util.ClassUtils;
|
||||
public final class JettyClientHttpRequestFactoryBuilder
|
||||
extends AbstractClientHttpRequestFactoryBuilder<JettyClientHttpRequestFactory> {
|
||||
|
||||
private final Consumer<HttpClient> httpClientCustomizer;
|
||||
|
||||
private final Consumer<HttpClientTransport> httpClientTransportCustomizer;
|
||||
|
||||
private final Consumer<ClientConnector> clientConnectorCustomizerCustomizer;
|
||||
private final JettyHttpClientBuilder httpClientBuilder;
|
||||
|
||||
JettyClientHttpRequestFactoryBuilder() {
|
||||
this(null, emptyCustomizer(), emptyCustomizer(), emptyCustomizer());
|
||||
this(null, new JettyHttpClientBuilder());
|
||||
}
|
||||
|
||||
private JettyClientHttpRequestFactoryBuilder(List<Consumer<JettyClientHttpRequestFactory>> customizers,
|
||||
Consumer<HttpClient> httpClientCustomizer, Consumer<HttpClientTransport> httpClientTransportCustomizer,
|
||||
Consumer<ClientConnector> clientConnectorCustomizerCustomizer) {
|
||||
JettyHttpClientBuilder httpClientBuilder) {
|
||||
super(customizers);
|
||||
this.httpClientCustomizer = httpClientCustomizer;
|
||||
this.httpClientTransportCustomizer = httpClientTransportCustomizer;
|
||||
this.clientConnectorCustomizerCustomizer = clientConnectorCustomizerCustomizer;
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JettyClientHttpRequestFactoryBuilder withCustomizer(Consumer<JettyClientHttpRequestFactory> customizer) {
|
||||
return new JettyClientHttpRequestFactoryBuilder(mergedCustomizers(customizer), this.httpClientCustomizer,
|
||||
this.httpClientTransportCustomizer, this.clientConnectorCustomizerCustomizer);
|
||||
return new JettyClientHttpRequestFactoryBuilder(mergedCustomizers(customizer), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JettyClientHttpRequestFactoryBuilder withCustomizers(
|
||||
Collection<Consumer<JettyClientHttpRequestFactory>> customizers) {
|
||||
return new JettyClientHttpRequestFactoryBuilder(mergedCustomizers(customizers), this.httpClientCustomizer,
|
||||
this.httpClientTransportCustomizer, this.clientConnectorCustomizerCustomizer);
|
||||
return new JettyClientHttpRequestFactoryBuilder(mergedCustomizers(customizers), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,8 +73,7 @@ public final class JettyClientHttpRequestFactoryBuilder
|
||||
public JettyClientHttpRequestFactoryBuilder withHttpClientCustomizer(Consumer<HttpClient> httpClientCustomizer) {
|
||||
Assert.notNull(httpClientCustomizer, "'httpClientCustomizer' must not be null");
|
||||
return new JettyClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientCustomizer.andThen(httpClientCustomizer), this.httpClientTransportCustomizer,
|
||||
this.clientConnectorCustomizerCustomizer);
|
||||
this.httpClientBuilder.withCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,9 +85,8 @@ public final class JettyClientHttpRequestFactoryBuilder
|
||||
public JettyClientHttpRequestFactoryBuilder withHttpClientTransportCustomizer(
|
||||
Consumer<HttpClientTransport> httpClientTransportCustomizer) {
|
||||
Assert.notNull(httpClientTransportCustomizer, "'httpClientTransportCustomizer' must not be null");
|
||||
return new JettyClientHttpRequestFactoryBuilder(getCustomizers(), this.httpClientCustomizer,
|
||||
this.httpClientTransportCustomizer.andThen(httpClientTransportCustomizer),
|
||||
this.clientConnectorCustomizerCustomizer);
|
||||
return new JettyClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withHttpClientTransportCustomizer(httpClientTransportCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,68 +98,20 @@ public final class JettyClientHttpRequestFactoryBuilder
|
||||
public JettyClientHttpRequestFactoryBuilder withClientConnectorCustomizerCustomizer(
|
||||
Consumer<ClientConnector> clientConnectorCustomizerCustomizer) {
|
||||
Assert.notNull(clientConnectorCustomizerCustomizer, "'clientConnectorCustomizerCustomizer' must not be null");
|
||||
return new JettyClientHttpRequestFactoryBuilder(getCustomizers(), this.httpClientCustomizer,
|
||||
this.httpClientTransportCustomizer,
|
||||
this.clientConnectorCustomizerCustomizer.andThen(clientConnectorCustomizerCustomizer));
|
||||
return new JettyClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withClientConnectorCustomizerCustomizer(clientConnectorCustomizerCustomizer));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JettyClientHttpRequestFactory createClientHttpRequestFactory(ClientHttpRequestFactorySettings settings) {
|
||||
JettyClientHttpRequestFactory requestFactory = createRequestFactory(settings);
|
||||
HttpClient httpClient = this.httpClientBuilder.build(asHttpClientSettings(settings.withTimeouts(null, null)));
|
||||
JettyClientHttpRequestFactory requestFactory = new JettyClientHttpRequestFactory(httpClient);
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(settings::connectTimeout).asInt(Duration::toMillis).to(requestFactory::setConnectTimeout);
|
||||
map.from(settings::readTimeout).asInt(Duration::toMillis).to(requestFactory::setReadTimeout);
|
||||
return requestFactory;
|
||||
}
|
||||
|
||||
private JettyClientHttpRequestFactory createRequestFactory(ClientHttpRequestFactorySettings settings) {
|
||||
HttpClientTransport transport = createTransport(settings);
|
||||
this.httpClientTransportCustomizer.accept(transport);
|
||||
HttpClient httpClient = new HttpClient(transport);
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(settings::redirects).as(this::followRedirects).to(httpClient::setFollowRedirects);
|
||||
this.httpClientCustomizer.accept(httpClient);
|
||||
return new JettyClientHttpRequestFactory(httpClient);
|
||||
}
|
||||
|
||||
private HttpClientTransport createTransport(ClientHttpRequestFactorySettings settings) {
|
||||
ClientConnector connector = createClientConnector(settings.sslBundle());
|
||||
return (connector.getSslContextFactory() != null) ? new HttpClientTransportDynamic(connector)
|
||||
: new HttpClientTransportOverHTTP(connector);
|
||||
}
|
||||
|
||||
private ClientConnector createClientConnector(SslBundle sslBundle) {
|
||||
ClientConnector connector = new ClientConnector();
|
||||
if (sslBundle != null) {
|
||||
connector.setSslContextFactory(createSslContextFactory(sslBundle));
|
||||
}
|
||||
this.clientConnectorCustomizerCustomizer.accept(connector);
|
||||
return connector;
|
||||
}
|
||||
|
||||
private SslContextFactory.Client createSslContextFactory(SslBundle sslBundle) {
|
||||
SslOptions options = sslBundle.getOptions();
|
||||
SSLContext sslContext = sslBundle.createSslContext();
|
||||
SslContextFactory.Client factory = new SslContextFactory.Client();
|
||||
factory.setSslContext(sslContext);
|
||||
if (options.getCiphers() != null) {
|
||||
factory.setIncludeCipherSuites(options.getCiphers());
|
||||
factory.setExcludeCipherSuites();
|
||||
}
|
||||
if (options.getEnabledProtocols() != null) {
|
||||
factory.setIncludeProtocols(options.getEnabledProtocols());
|
||||
factory.setExcludeProtocols();
|
||||
}
|
||||
return factory;
|
||||
}
|
||||
|
||||
private boolean followRedirects(Redirects redirects) {
|
||||
return switch (redirects) {
|
||||
case FOLLOW_WHEN_POSSIBLE, FOLLOW -> true;
|
||||
case DONT_FOLLOW -> false;
|
||||
};
|
||||
}
|
||||
|
||||
static class Classes {
|
||||
|
||||
static final String HTTP_CLIENT = "org.eclipse.jetty.client.HttpClient";
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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.http.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpClientTransport;
|
||||
import org.eclipse.jetty.client.Request;
|
||||
import org.eclipse.jetty.client.transport.HttpClientTransportDynamic;
|
||||
import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP;
|
||||
import org.eclipse.jetty.io.ClientConnector;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Builder that can be used to create a Jetty {@link HttpClient}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public final class JettyHttpClientBuilder {
|
||||
|
||||
private final Consumer<HttpClient> customizer;
|
||||
|
||||
private final Consumer<HttpClientTransport> httpClientTransportCustomizer;
|
||||
|
||||
private final Consumer<ClientConnector> clientConnectorCustomizerCustomizer;
|
||||
|
||||
public JettyHttpClientBuilder() {
|
||||
this(Empty.consumer(), Empty.consumer(), Empty.consumer());
|
||||
}
|
||||
|
||||
private JettyHttpClientBuilder(Consumer<HttpClient> customizer,
|
||||
Consumer<HttpClientTransport> httpClientTransportCustomizer,
|
||||
Consumer<ClientConnector> clientConnectorCustomizerCustomizer) {
|
||||
this.customizer = customizer;
|
||||
this.httpClientTransportCustomizer = httpClientTransportCustomizer;
|
||||
this.clientConnectorCustomizerCustomizer = clientConnectorCustomizerCustomizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JettyClientHttpRequestFactoryBuilder} that applies additional
|
||||
* customization to the underlying {@link HttpClient}.
|
||||
* @param customizer the customizer to apply
|
||||
* @return a new {@link JettyClientHttpRequestFactoryBuilder} instance
|
||||
*/
|
||||
public JettyHttpClientBuilder withCustomizer(Consumer<HttpClient> customizer) {
|
||||
Assert.notNull(customizer, "'customizer' must not be null");
|
||||
return new JettyHttpClientBuilder(this.customizer.andThen(customizer), this.httpClientTransportCustomizer,
|
||||
this.clientConnectorCustomizerCustomizer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JettyClientHttpRequestFactoryBuilder} that applies additional
|
||||
* customization to the underlying {@link HttpClientTransport}.
|
||||
* @param httpClientTransportCustomizer the customizer to apply
|
||||
* @return a new {@link JettyClientHttpRequestFactoryBuilder} instance
|
||||
*/
|
||||
public JettyHttpClientBuilder withHttpClientTransportCustomizer(
|
||||
Consumer<HttpClientTransport> httpClientTransportCustomizer) {
|
||||
Assert.notNull(httpClientTransportCustomizer, "'httpClientTransportCustomizer' must not be null");
|
||||
return new JettyHttpClientBuilder(this.customizer,
|
||||
this.httpClientTransportCustomizer.andThen(httpClientTransportCustomizer),
|
||||
this.clientConnectorCustomizerCustomizer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JettyClientHttpRequestFactoryBuilder} that applies additional
|
||||
* customization to the underlying {@link ClientConnector}.
|
||||
* @param clientConnectorCustomizerCustomizer the customizer to apply
|
||||
* @return a new {@link JettyClientHttpRequestFactoryBuilder} instance
|
||||
*/
|
||||
public JettyHttpClientBuilder withClientConnectorCustomizerCustomizer(
|
||||
Consumer<ClientConnector> clientConnectorCustomizerCustomizer) {
|
||||
Assert.notNull(clientConnectorCustomizerCustomizer, "'clientConnectorCustomizerCustomizer' must not be null");
|
||||
return new JettyHttpClientBuilder(this.customizer, this.httpClientTransportCustomizer,
|
||||
this.clientConnectorCustomizerCustomizer.andThen(clientConnectorCustomizerCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new {@link HttpClient} instance with the given settings applied.
|
||||
* @param settings the settings to apply
|
||||
* @return a new {@link HttpClient} instance
|
||||
*/
|
||||
public HttpClient build(HttpClientSettings settings) {
|
||||
settings = (settings != null) ? settings : HttpClientSettings.DEFAULTS;
|
||||
HttpClientTransport transport = createTransport(settings);
|
||||
this.httpClientTransportCustomizer.accept(transport);
|
||||
HttpClient httpClient = createHttpClient(settings.readTimeout(), transport);
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(settings::connectTimeout).as(Duration::toMillis).to(httpClient::setConnectTimeout);
|
||||
map.from(settings::redirects).as(this::followRedirects).to(httpClient::setFollowRedirects);
|
||||
this.customizer.accept(httpClient);
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
private HttpClient createHttpClient(Duration readTimeout, HttpClientTransport transport) {
|
||||
return (readTimeout != null) ? new HttpClientWithReadTimeout(transport, readTimeout)
|
||||
: new HttpClient(transport);
|
||||
}
|
||||
|
||||
private HttpClientTransport createTransport(HttpClientSettings settings) {
|
||||
ClientConnector connector = createClientConnector(settings.sslBundle());
|
||||
return (connector.getSslContextFactory() != null) ? new HttpClientTransportDynamic(connector)
|
||||
: new HttpClientTransportOverHTTP(connector);
|
||||
}
|
||||
|
||||
private ClientConnector createClientConnector(SslBundle sslBundle) {
|
||||
ClientConnector connector = new ClientConnector();
|
||||
if (sslBundle != null) {
|
||||
connector.setSslContextFactory(createSslContextFactory(sslBundle));
|
||||
}
|
||||
this.clientConnectorCustomizerCustomizer.accept(connector);
|
||||
return connector;
|
||||
}
|
||||
|
||||
private SslContextFactory.Client createSslContextFactory(SslBundle sslBundle) {
|
||||
SslOptions options = sslBundle.getOptions();
|
||||
SSLContext sslContext = sslBundle.createSslContext();
|
||||
SslContextFactory.Client factory = new SslContextFactory.Client();
|
||||
factory.setSslContext(sslContext);
|
||||
if (options.getCiphers() != null) {
|
||||
factory.setIncludeCipherSuites(options.getCiphers());
|
||||
factory.setExcludeCipherSuites();
|
||||
}
|
||||
if (options.getEnabledProtocols() != null) {
|
||||
factory.setIncludeProtocols(options.getEnabledProtocols());
|
||||
factory.setExcludeProtocols();
|
||||
}
|
||||
return factory;
|
||||
}
|
||||
|
||||
private boolean followRedirects(HttpRedirects redirects) {
|
||||
return switch (redirects) {
|
||||
case FOLLOW_WHEN_POSSIBLE, FOLLOW -> true;
|
||||
case DONT_FOLLOW -> false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link HttpClient} subclass that sets the read timeout.
|
||||
*/
|
||||
static class HttpClientWithReadTimeout extends HttpClient {
|
||||
|
||||
private final Duration readTimeout;
|
||||
|
||||
HttpClientWithReadTimeout(HttpClientTransport transport, Duration readTimeout) {
|
||||
super(transport);
|
||||
this.readTimeout = readTimeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.jetty.client.Request newRequest(java.net.URI uri) {
|
||||
Request request = super.newRequest(uri);
|
||||
request.timeout(this.readTimeout.toMillis(), TimeUnit.MILLISECONDS);
|
||||
return request;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -22,21 +22,12 @@ import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
|
||||
import io.netty.handler.ssl.SslContextBuilder;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
import reactor.netty.tcp.SslProvider.SslContextSpec;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslManagerBundle;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.http.client.ReactorClientHttpRequestFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.function.ThrowingConsumer;
|
||||
|
||||
/**
|
||||
* Builder for {@link ClientHttpRequestFactoryBuilder#reactor()}.
|
||||
@@ -49,27 +40,27 @@ import org.springframework.util.function.ThrowingConsumer;
|
||||
public final class ReactorClientHttpRequestFactoryBuilder
|
||||
extends AbstractClientHttpRequestFactoryBuilder<ReactorClientHttpRequestFactory> {
|
||||
|
||||
private final UnaryOperator<HttpClient> httpClientCustomizer;
|
||||
private final ReactorHttpClientBuilder httpClientBuilder;
|
||||
|
||||
ReactorClientHttpRequestFactoryBuilder() {
|
||||
this(null, UnaryOperator.identity());
|
||||
this(null, new ReactorHttpClientBuilder());
|
||||
}
|
||||
|
||||
private ReactorClientHttpRequestFactoryBuilder(List<Consumer<ReactorClientHttpRequestFactory>> customizers,
|
||||
UnaryOperator<HttpClient> httpClientCustomizer) {
|
||||
ReactorHttpClientBuilder httpClientBuilder) {
|
||||
super(customizers);
|
||||
this.httpClientCustomizer = httpClientCustomizer;
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactorClientHttpRequestFactoryBuilder withCustomizer(Consumer<ReactorClientHttpRequestFactory> customizer) {
|
||||
return new ReactorClientHttpRequestFactoryBuilder(mergedCustomizers(customizer), this.httpClientCustomizer);
|
||||
return new ReactorClientHttpRequestFactoryBuilder(mergedCustomizers(customizer), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactorClientHttpRequestFactoryBuilder withCustomizers(
|
||||
Collection<Consumer<ReactorClientHttpRequestFactory>> customizers) {
|
||||
return new ReactorClientHttpRequestFactoryBuilder(mergedCustomizers(customizers), this.httpClientCustomizer);
|
||||
return new ReactorClientHttpRequestFactoryBuilder(mergedCustomizers(customizers), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,52 +73,20 @@ public final class ReactorClientHttpRequestFactoryBuilder
|
||||
UnaryOperator<HttpClient> httpClientCustomizer) {
|
||||
Assert.notNull(httpClientCustomizer, "'httpClientCustomizer' must not be null");
|
||||
return new ReactorClientHttpRequestFactoryBuilder(getCustomizers(),
|
||||
(t) -> httpClientCustomizer.apply(this.httpClientCustomizer.apply(t)));
|
||||
this.httpClientBuilder.withHttpClientCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ReactorClientHttpRequestFactory createClientHttpRequestFactory(
|
||||
ClientHttpRequestFactorySettings settings) {
|
||||
ReactorClientHttpRequestFactory requestFactory = createRequestFactory(settings);
|
||||
HttpClient httpClient = this.httpClientBuilder.build(asHttpClientSettings(settings.withTimeouts(null, null)));
|
||||
ReactorClientHttpRequestFactory requestFactory = new ReactorClientHttpRequestFactory(httpClient);
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(settings::connectTimeout).asInt(Duration::toMillis).to(requestFactory::setConnectTimeout);
|
||||
map.from(settings::readTimeout).asInt(Duration::toMillis).to(requestFactory::setReadTimeout);
|
||||
return requestFactory;
|
||||
}
|
||||
|
||||
private ReactorClientHttpRequestFactory createRequestFactory(ClientHttpRequestFactorySettings settings) {
|
||||
HttpClient httpClient = applyDefaults(HttpClient.create());
|
||||
httpClient = httpClient.followRedirect(followRedirects(settings.redirects()));
|
||||
if (settings.sslBundle() != null) {
|
||||
httpClient = httpClient.secure((ThrowingConsumer.of((spec) -> configureSsl(spec, settings.sslBundle()))));
|
||||
}
|
||||
httpClient = this.httpClientCustomizer.apply(httpClient);
|
||||
return new ReactorClientHttpRequestFactory(httpClient);
|
||||
}
|
||||
|
||||
private boolean followRedirects(Redirects redirects) {
|
||||
return switch (redirects) {
|
||||
case FOLLOW_WHEN_POSSIBLE, FOLLOW -> true;
|
||||
case DONT_FOLLOW -> false;
|
||||
};
|
||||
}
|
||||
|
||||
HttpClient applyDefaults(HttpClient httpClient) {
|
||||
// Aligns with ReactorClientHttpRequestFactory defaults
|
||||
return httpClient.compress(true);
|
||||
}
|
||||
|
||||
private void configureSsl(SslContextSpec spec, SslBundle sslBundle) throws SSLException {
|
||||
SslOptions options = sslBundle.getOptions();
|
||||
SslManagerBundle managers = sslBundle.getManagers();
|
||||
SslContextBuilder builder = SslContextBuilder.forClient()
|
||||
.keyManager(managers.getKeyManagerFactory())
|
||||
.trustManager(managers.getTrustManagerFactory())
|
||||
.ciphers(SslOptions.asSet(options.getCiphers()))
|
||||
.protocols(options.getEnabledProtocols());
|
||||
spec.sslContext(builder.build());
|
||||
}
|
||||
|
||||
static class Classes {
|
||||
|
||||
static final String HTTP_CLIENT = "reactor.netty.http.client.HttpClient";
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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.http.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.handler.ssl.SslContextBuilder;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
import reactor.netty.tcp.SslProvider.SslContextSpec;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslManagerBundle;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.function.ThrowingConsumer;
|
||||
|
||||
/**
|
||||
* Builder that can be used to create a Rector Netty {@link HttpClient}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public final class ReactorHttpClientBuilder {
|
||||
|
||||
private final UnaryOperator<HttpClient> customizer;
|
||||
|
||||
public ReactorHttpClientBuilder() {
|
||||
this(UnaryOperator.identity());
|
||||
}
|
||||
|
||||
private ReactorHttpClientBuilder(UnaryOperator<HttpClient> customizer) {
|
||||
this.customizer = customizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ReactorHttpClientBuilder} that applies additional customization
|
||||
* to the underlying {@link HttpClient}.
|
||||
* @param customizer the customizer to apply
|
||||
* @return a new {@link ReactorHttpClientBuilder} instance
|
||||
*/
|
||||
public ReactorHttpClientBuilder withHttpClientCustomizer(UnaryOperator<HttpClient> customizer) {
|
||||
Assert.notNull(customizer, "'customizer' must not be null");
|
||||
return new ReactorHttpClientBuilder((t) -> customizer.apply(this.customizer.apply(t)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new {@link HttpClient} instance with the given settings applied.
|
||||
* @param settings the settings to apply
|
||||
* @return a new {@link HttpClient} instance
|
||||
*/
|
||||
public HttpClient build(HttpClientSettings settings) {
|
||||
settings = (settings != null) ? settings : HttpClientSettings.DEFAULTS;
|
||||
HttpClient httpClient = applyDefaults(HttpClient.create());
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
httpClient = map.from(settings::connectTimeout).to(httpClient, this::setConnectTimeout);
|
||||
httpClient = map.from(settings::readTimeout).to(httpClient, HttpClient::responseTimeout);
|
||||
httpClient = map.from(settings::redirects).as(this::followRedirects).to(httpClient, HttpClient::followRedirect);
|
||||
httpClient = map.from(settings::sslBundle).to(httpClient, this::secure);
|
||||
return this.customizer.apply(httpClient);
|
||||
}
|
||||
|
||||
HttpClient applyDefaults(HttpClient httpClient) {
|
||||
// Aligns with Spring Framework defaults
|
||||
return httpClient.compress(true);
|
||||
}
|
||||
|
||||
private HttpClient setConnectTimeout(HttpClient httpClient, Duration timeout) {
|
||||
return httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) timeout.toMillis());
|
||||
}
|
||||
|
||||
private boolean followRedirects(HttpRedirects redirects) {
|
||||
return switch (redirects) {
|
||||
case FOLLOW_WHEN_POSSIBLE, FOLLOW -> true;
|
||||
case DONT_FOLLOW -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private HttpClient secure(HttpClient httpClient, SslBundle sslBundle) {
|
||||
return httpClient.secure((ThrowingConsumer.of((spec) -> configureSsl(spec, sslBundle))));
|
||||
}
|
||||
|
||||
private void configureSsl(SslContextSpec spec, SslBundle sslBundle) throws SSLException {
|
||||
SslOptions options = sslBundle.getOptions();
|
||||
SslManagerBundle managers = sslBundle.getManagers();
|
||||
SslContextBuilder builder = SslContextBuilder.forClient()
|
||||
.keyManager(managers.getKeyManagerFactory())
|
||||
.trustManager(managers.getTrustManagerFactory())
|
||||
.ciphers(SslOptions.asSet(options.getCiphers()))
|
||||
.protocols(options.getEnabledProtocols());
|
||||
spec.sslContext(builder.build());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.http.client.reactive;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.boot.http.client.HttpClientSettings;
|
||||
import org.springframework.boot.util.LambdaSafe;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Internal base class used for {@link ClientHttpConnectorBuilder} implementations.
|
||||
*
|
||||
* @param <T> the {@link ClientHttpConnector} type
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
abstract class AbstractClientHttpConnectorBuilder<T extends ClientHttpConnector>
|
||||
implements ClientHttpConnectorBuilder<T> {
|
||||
|
||||
private final List<Consumer<T>> customizers;
|
||||
|
||||
protected AbstractClientHttpConnectorBuilder(List<Consumer<T>> customizers) {
|
||||
this.customizers = (customizers != null) ? customizers : Collections.emptyList();
|
||||
}
|
||||
|
||||
protected final List<Consumer<T>> getCustomizers() {
|
||||
return this.customizers;
|
||||
}
|
||||
|
||||
protected final List<Consumer<T>> mergedCustomizers(Consumer<T> customizer) {
|
||||
Assert.notNull(this.customizers, "'customizer' must not be null");
|
||||
return merge(this.customizers, List.of(customizer));
|
||||
}
|
||||
|
||||
protected final List<Consumer<T>> mergedCustomizers(Collection<Consumer<T>> customizers) {
|
||||
Assert.notNull(customizers, "'customizers' must not be null");
|
||||
Assert.noNullElements(customizers, "'customizers' must not contain null elements");
|
||||
return merge(this.customizers, customizers);
|
||||
}
|
||||
|
||||
private <E> List<E> merge(Collection<E> list, Collection<? extends E> additional) {
|
||||
List<E> merged = new ArrayList<>(list);
|
||||
merged.addAll(additional);
|
||||
return List.copyOf(merged);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public final T build(ClientHttpConnectorSettings settings) {
|
||||
T connector = createClientHttpConnector((settings != null) ? settings : ClientHttpConnectorSettings.defaults());
|
||||
LambdaSafe.callbacks(Consumer.class, this.customizers, connector)
|
||||
.invoke((consumer) -> consumer.accept(connector));
|
||||
return connector;
|
||||
}
|
||||
|
||||
protected abstract T createClientHttpConnector(ClientHttpConnectorSettings settings);
|
||||
|
||||
protected final HttpClientSettings asHttpClientSettings(ClientHttpConnectorSettings settings) {
|
||||
return (settings != null) ? new HttpClientSettings(settings.redirects(), settings.connectTimeout(),
|
||||
settings.readTimeout(), settings.sslBundle()) : null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* 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.http.client.reactive;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.boot.util.LambdaSafe;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.JdkClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.JettyClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Interface used to build a fully configured {@link ClientHttpConnector}. Builders for
|
||||
* {@link #reactor() Reactor}, {@link #jetty() Jetty}, {@link #httpComponents() Apache
|
||||
* HTTP Components} and {@link #jdk() JDK} can be obtained using the factory methods on
|
||||
* this interface. The {@link #of(Class)} method may be used to instantiate based on the
|
||||
* connector type.
|
||||
*
|
||||
* @param <T> the {@link ClientHttpConnector} type
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ClientHttpConnectorBuilder<T extends ClientHttpConnector> {
|
||||
|
||||
/**
|
||||
* Build a default configured {@link ClientHttpConnectorBuilder}.
|
||||
* @return a default configured {@link ClientHttpConnectorBuilder}.
|
||||
*/
|
||||
default T build() {
|
||||
return build(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a fully configured {@link ClientHttpConnector}, applying the given
|
||||
* {@code settings} if they are provided.
|
||||
* @param settings the settings to apply or {@code null}
|
||||
* @return a fully configured {@link ClientHttpConnector}.
|
||||
*/
|
||||
T build(ClientHttpConnectorSettings settings);
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpConnectorBuilder} that applies the given customizer
|
||||
* to the {@link ClientHttpConnector} after it has been built.
|
||||
* @param customizer the customizers to apply
|
||||
* @return a new {@link ClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
default ClientHttpConnectorBuilder<T> withCustomizer(Consumer<T> customizer) {
|
||||
return withCustomizers(List.of(customizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpConnectorBuilder} that applies the given customizers
|
||||
* to the {@link ClientHttpConnector} after it has been built.
|
||||
* @param customizers the customizers to apply
|
||||
* @return a new {@link ClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
default ClientHttpConnectorBuilder<T> withCustomizers(Collection<Consumer<T>> customizers) {
|
||||
Assert.notNull(customizers, "'customizers' must not be null");
|
||||
Assert.noNullElements(customizers, "'customizers' must not contain null elements");
|
||||
return (settings) -> {
|
||||
T factory = build(settings);
|
||||
LambdaSafe.callbacks(Consumer.class, customizers, factory).invoke((consumer) -> consumer.accept(factory));
|
||||
return factory;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link HttpComponentsClientHttpConnectorBuilder} that can be used to build
|
||||
* a {@link HttpComponentsClientHttpConnector}.
|
||||
* @return a new {@link HttpComponentsClientHttpConnectorBuilder}
|
||||
*/
|
||||
static HttpComponentsClientHttpConnectorBuilder httpComponents() {
|
||||
return new HttpComponentsClientHttpConnectorBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link JettyClientHttpConnectorBuilder} that can be used to build a
|
||||
* {@link JettyClientHttpConnector}.
|
||||
* @return a new {@link JettyClientHttpConnectorBuilder}
|
||||
*/
|
||||
static JettyClientHttpConnectorBuilder jetty() {
|
||||
return new JettyClientHttpConnectorBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link ReactorClientHttpConnectorBuilder} that can be used to build a
|
||||
* {@link ReactorClientHttpConnector}.
|
||||
* @return a new {@link ReactorClientHttpConnectorBuilder}
|
||||
*/
|
||||
static ReactorClientHttpConnectorBuilder reactor() {
|
||||
return new ReactorClientHttpConnectorBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link JdkClientHttpConnectorBuilder} that can be used to build a
|
||||
* {@link JdkClientHttpConnector} .
|
||||
* @return a new {@link JdkClientHttpConnectorBuilder}
|
||||
*/
|
||||
static JdkClientHttpConnectorBuilder jdk() {
|
||||
return new JdkClientHttpConnectorBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpConnectorBuilder} for the given
|
||||
* {@code requestFactoryType}. The following implementations are supported:
|
||||
* <ul>
|
||||
* <li>{@link ReactorClientHttpConnector}</li>
|
||||
* <li>{@link JettyClientHttpConnector}</li>
|
||||
* <li>{@link HttpComponentsClientHttpConnector}</li>
|
||||
* <li>{@link JdkClientHttpConnector}</li>
|
||||
* </ul>
|
||||
* @param <T> the {@link ClientHttpConnector} type
|
||||
* @param clientHttpConnectorType the {@link ClientHttpConnector} type
|
||||
* @return a new {@link ClientHttpConnectorBuilder}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T extends ClientHttpConnector> ClientHttpConnectorBuilder<T> of(Class<T> clientHttpConnectorType) {
|
||||
Assert.notNull(clientHttpConnectorType, "'requestFactoryType' must not be null");
|
||||
Assert.isTrue(clientHttpConnectorType != ClientHttpConnector.class,
|
||||
"'clientHttpConnectorType' must be an implementation of ClientHttpConnector");
|
||||
if (clientHttpConnectorType == ReactorClientHttpConnector.class) {
|
||||
return (ClientHttpConnectorBuilder<T>) reactor();
|
||||
}
|
||||
if (clientHttpConnectorType == JettyClientHttpConnector.class) {
|
||||
return (ClientHttpConnectorBuilder<T>) jetty();
|
||||
}
|
||||
if (clientHttpConnectorType == HttpComponentsClientHttpConnector.class) {
|
||||
return (ClientHttpConnectorBuilder<T>) httpComponents();
|
||||
}
|
||||
if (clientHttpConnectorType == JdkClientHttpConnector.class) {
|
||||
return (ClientHttpConnectorBuilder<T>) jdk();
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
"'clientHttpConnectorType' %s is not supported".formatted(clientHttpConnectorType.getName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the most suitable {@link ClientHttpConnectorBuilder} based on the classpath.
|
||||
* The methods favors builders in the following order:
|
||||
* <ol>
|
||||
* <li>{@link #reactor()}</li>
|
||||
* <li>{@link #jetty()}</li>
|
||||
* <li>{@link #httpComponents()}</li>
|
||||
* <li>{@link #jdk()}</li>
|
||||
* </ol>
|
||||
* @return the most suitable {@link ClientHttpConnectorBuilder} for the classpath
|
||||
*/
|
||||
static ClientHttpConnectorBuilder<? extends ClientHttpConnector> detect() {
|
||||
return detect(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the most suitable {@link ClientHttpConnectorBuilder} based on the classpath.
|
||||
* The methods favors builders in the following order:
|
||||
* <ol>
|
||||
* <li>{@link #reactor()}</li>
|
||||
* <li>{@link #jetty()}</li>
|
||||
* <li>{@link #httpComponents()}</li>
|
||||
* <li>{@link #jdk()}</li>
|
||||
* </ol>
|
||||
* @param classLoader the class loader to use for detection
|
||||
* @return the most suitable {@link ClientHttpConnectorBuilder} for the classpath
|
||||
*/
|
||||
static ClientHttpConnectorBuilder<? extends ClientHttpConnector> detect(ClassLoader classLoader) {
|
||||
if (ReactorClientHttpConnectorBuilder.Classes.present(classLoader)) {
|
||||
return reactor();
|
||||
}
|
||||
if (JettyClientHttpConnectorBuilder.Classes.present(classLoader)) {
|
||||
return jetty();
|
||||
}
|
||||
if (HttpComponentsClientHttpConnectorBuilder.Classes.present(classLoader)) {
|
||||
return httpComponents();
|
||||
}
|
||||
if (JdkClientHttpConnectorBuilder.Classes.present(classLoader)) {
|
||||
return jdk();
|
||||
}
|
||||
throw new IllegalStateException("Unable to detect any ClientHttpConnectorBuilder");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.http.client.reactive;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.boot.http.client.HttpRedirects;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
|
||||
/**
|
||||
* Settings that can be applied when creating a {@link ClientHttpConnector}.
|
||||
*
|
||||
* @param redirects the follow redirect strategy to use or null to redirect whenever the
|
||||
* underlying library allows it
|
||||
* @param connectTimeout the connect timeout
|
||||
* @param readTimeout the read timeout
|
||||
* @param sslBundle the SSL bundle providing SSL configuration
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
* @see ClientHttpConnectorBuilder
|
||||
*/
|
||||
public record ClientHttpConnectorSettings(HttpRedirects redirects, Duration connectTimeout, Duration readTimeout,
|
||||
SslBundle sslBundle) {
|
||||
|
||||
private static final ClientHttpConnectorSettings defaults = new ClientHttpConnectorSettings(null, null, null, null);
|
||||
|
||||
public ClientHttpConnectorSettings {
|
||||
redirects = (redirects != null) ? redirects : HttpRedirects.FOLLOW_WHEN_POSSIBLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpConnectorSettings} instance with an updated connect
|
||||
* timeout setting.
|
||||
* @param connectTimeout the new connect timeout setting
|
||||
* @return a new {@link ClientHttpConnectorSettings} instance
|
||||
*/
|
||||
public ClientHttpConnectorSettings withConnectTimeout(Duration connectTimeout) {
|
||||
return new ClientHttpConnectorSettings(this.redirects, connectTimeout, this.readTimeout, this.sslBundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpConnectorSettings} instance with an updated read
|
||||
* timeout setting.
|
||||
* @param readTimeout the new read timeout setting
|
||||
* @return a new {@link ClientHttpConnectorSettings} instance
|
||||
*/
|
||||
public ClientHttpConnectorSettings withReadTimeout(Duration readTimeout) {
|
||||
return new ClientHttpConnectorSettings(this.redirects, this.connectTimeout, readTimeout, this.sslBundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpConnectorSettings} instance with an updated connect
|
||||
* and read timeout setting.
|
||||
* @param connectTimeout the new connect timeout setting
|
||||
* @param readTimeout the new read timeout setting
|
||||
* @return a new {@link ClientHttpConnectorSettings} instance
|
||||
*/
|
||||
public ClientHttpConnectorSettings withTimeouts(Duration connectTimeout, Duration readTimeout) {
|
||||
return new ClientHttpConnectorSettings(this.redirects, connectTimeout, readTimeout, this.sslBundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpConnectorSettings} instance with an updated SSL
|
||||
* bundle setting.
|
||||
* @param sslBundle the new SSL bundle setting
|
||||
* @return a new {@link ClientHttpConnectorSettings} instance
|
||||
*/
|
||||
public ClientHttpConnectorSettings withSslBundle(SslBundle sslBundle) {
|
||||
return new ClientHttpConnectorSettings(this.redirects, this.connectTimeout, this.readTimeout, sslBundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpConnectorSettings} instance with an updated redirect
|
||||
* setting.
|
||||
* @param redirects the new redirects setting
|
||||
* @return a new {@link ClientHttpConnectorSettings} instance
|
||||
*/
|
||||
public ClientHttpConnectorSettings withRedirects(HttpRedirects redirects) {
|
||||
return new ClientHttpConnectorSettings(redirects, this.connectTimeout, this.readTimeout, this.sslBundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ClientHttpConnectorSettings} using defaults for all settings
|
||||
* other than the provided SSL bundle.
|
||||
* @param sslBundle the SSL bundle setting
|
||||
* @return a new {@link ClientHttpConnectorSettings} instance
|
||||
*/
|
||||
public static ClientHttpConnectorSettings ofSslBundle(SslBundle sslBundle) {
|
||||
return defaults().withSslBundle(sslBundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use defaults for the {@link ClientHttpConnector} which can differ depending on the
|
||||
* implementation.
|
||||
* @return default settings
|
||||
*/
|
||||
public static ClientHttpConnectorSettings defaults() {
|
||||
return defaults;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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.http.client.reactive;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.hc.client5.http.config.ConnectionConfig;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
|
||||
import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
|
||||
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
|
||||
import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
|
||||
|
||||
import org.springframework.boot.http.client.HttpComponentsHttpAsyncClientBuilder;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Builder for {@link ClientHttpConnectorBuilder#httpComponents()}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public final class HttpComponentsClientHttpConnectorBuilder
|
||||
extends AbstractClientHttpConnectorBuilder<HttpComponentsClientHttpConnector> {
|
||||
|
||||
private final HttpComponentsHttpAsyncClientBuilder httpClientBuilder;
|
||||
|
||||
HttpComponentsClientHttpConnectorBuilder() {
|
||||
this(null, new HttpComponentsHttpAsyncClientBuilder());
|
||||
}
|
||||
|
||||
private HttpComponentsClientHttpConnectorBuilder(List<Consumer<HttpComponentsClientHttpConnector>> customizers,
|
||||
HttpComponentsHttpAsyncClientBuilder httpClientBuilder) {
|
||||
super(customizers);
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsClientHttpConnectorBuilder} that applies
|
||||
* additional customization to the underlying {@link HttpAsyncClientBuilder}.
|
||||
* @param httpClientCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsHttpAsyncClientBuilder} instance
|
||||
*/
|
||||
public HttpComponentsClientHttpConnectorBuilder withHttpClientCustomizer(
|
||||
Consumer<HttpAsyncClientBuilder> httpClientCustomizer) {
|
||||
Assert.notNull(httpClientCustomizer, "'customizer' must not be null");
|
||||
return new HttpComponentsClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsClientHttpConnectorBuilder} that applies
|
||||
* additional customization to the underlying
|
||||
* {@link PoolingAsyncClientConnectionManagerBuilder}.
|
||||
* @param connectionManagerCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
public HttpComponentsClientHttpConnectorBuilder withConnectionManagerCustomizer(
|
||||
Consumer<PoolingAsyncClientConnectionManagerBuilder> connectionManagerCustomizer) {
|
||||
Assert.notNull(connectionManagerCustomizer, "'connectionManagerCustomizer' must not be null");
|
||||
return new HttpComponentsClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withConnectionManagerCustomizer(connectionManagerCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsClientHttpConnectorBuilder} that applies
|
||||
* additional customization to the underlying
|
||||
* {@link org.apache.hc.client5.http.config.ConnectionConfig.Builder}.
|
||||
* @param connectionConfigCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
public HttpComponentsClientHttpConnectorBuilder withConnectionConfigCustomizer(
|
||||
Consumer<ConnectionConfig.Builder> connectionConfigCustomizer) {
|
||||
Assert.notNull(connectionConfigCustomizer, "'connectionConfigCustomizer' must not be null");
|
||||
return new HttpComponentsClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withConnectionConfigCustomizer(connectionConfigCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsClientHttpConnectorBuilder} with a replacement
|
||||
* {@link TlsStrategy} factory.
|
||||
* @param tlsStrategyFactory the new factory used to create a {@link TlsStrategy} for
|
||||
* a given {@link SslBundle}
|
||||
* @return a new {@link HttpComponentsClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
public HttpComponentsClientHttpConnectorBuilder withTlsSocketStrategyFactory(
|
||||
Function<SslBundle, TlsStrategy> tlsStrategyFactory) {
|
||||
Assert.notNull(tlsStrategyFactory, "'tlsStrategyFactory' must not be null");
|
||||
return new HttpComponentsClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withTlsStrategyFactory(tlsStrategyFactory));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link HttpComponentsClientHttpConnectorBuilder} that applies
|
||||
* additional customization to the underlying
|
||||
* {@link org.apache.hc.client5.http.config.RequestConfig.Builder} used for default
|
||||
* requests.
|
||||
* @param defaultRequestConfigCustomizer the customizer to apply
|
||||
* @return a new {@link HttpComponentsClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
public HttpComponentsClientHttpConnectorBuilder withDefaultRequestConfigCustomizer(
|
||||
Consumer<RequestConfig.Builder> defaultRequestConfigCustomizer) {
|
||||
Assert.notNull(defaultRequestConfigCustomizer, "'defaultRequestConfigCustomizer' must not be null");
|
||||
return new HttpComponentsClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withDefaultRequestConfigCustomizer(defaultRequestConfigCustomizer));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpComponentsClientHttpConnector createClientHttpConnector(ClientHttpConnectorSettings settings) {
|
||||
CloseableHttpAsyncClient client = this.httpClientBuilder.build(asHttpClientSettings(settings));
|
||||
return new HttpComponentsClientHttpConnector(client);
|
||||
}
|
||||
|
||||
static class Classes {
|
||||
|
||||
static final String HTTP_CLIENTS = "org.apache.hc.client5.http.impl.async.HttpAsyncClients";
|
||||
|
||||
static final String REACTIVE_RESPONSE_CONSUMER = "org.apache.hc.core5.reactive.ReactiveResponseConsumer";
|
||||
|
||||
static boolean present(ClassLoader classLoader) {
|
||||
return ClassUtils.isPresent(HTTP_CLIENTS, classLoader)
|
||||
&& ClassUtils.isPresent(REACTIVE_RESPONSE_CONSUMER, classLoader);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.http.client.reactive;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.http.client.JdkHttpClientBuilder;
|
||||
import org.springframework.http.client.reactive.JdkClientHttpConnector;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Builder for {@link ClientHttpConnectorBuilder#jdk()}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public final class JdkClientHttpConnectorBuilder extends AbstractClientHttpConnectorBuilder<JdkClientHttpConnector> {
|
||||
|
||||
private final JdkHttpClientBuilder httpClientBuilder;
|
||||
|
||||
JdkClientHttpConnectorBuilder() {
|
||||
this(null, new JdkHttpClientBuilder());
|
||||
}
|
||||
|
||||
private JdkClientHttpConnectorBuilder(List<Consumer<JdkClientHttpConnector>> customizers,
|
||||
JdkHttpClientBuilder httpClientBuilder) {
|
||||
super(customizers);
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JdkClientHttpConnectorBuilder withCustomizer(Consumer<JdkClientHttpConnector> customizer) {
|
||||
return new JdkClientHttpConnectorBuilder(mergedCustomizers(customizer), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JdkClientHttpConnectorBuilder withCustomizers(Collection<Consumer<JdkClientHttpConnector>> customizers) {
|
||||
return new JdkClientHttpConnectorBuilder(mergedCustomizers(customizers), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JdkClientHttpConnectorBuilder} that applies additional
|
||||
* customization to the underlying {@link java.net.http.HttpClient.Builder}.
|
||||
* @param httpClientCustomizer the customizer to apply
|
||||
* @return a new {@link JdkClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
public JdkClientHttpConnectorBuilder withHttpClientCustomizer(Consumer<HttpClient.Builder> httpClientCustomizer) {
|
||||
Assert.notNull(httpClientCustomizer, "'httpClientCustomizer' must not be null");
|
||||
return new JdkClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JdkClientHttpConnector createClientHttpConnector(ClientHttpConnectorSettings settings) {
|
||||
HttpClient httpClient = this.httpClientBuilder.build(asHttpClientSettings(settings.withReadTimeout(null)));
|
||||
JdkClientHttpConnector connector = new JdkClientHttpConnector(httpClient);
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(settings::readTimeout).to(connector::setReadTimeout);
|
||||
return connector;
|
||||
}
|
||||
|
||||
static class Classes {
|
||||
|
||||
static final String HTTP_CLIENT = "java.net.http.HttpClient";
|
||||
|
||||
static boolean present(ClassLoader classLoader) {
|
||||
return ClassUtils.isPresent(HTTP_CLIENT, classLoader);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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.http.client.reactive;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpClientTransport;
|
||||
import org.eclipse.jetty.io.ClientConnector;
|
||||
|
||||
import org.springframework.boot.http.client.JettyHttpClientBuilder;
|
||||
import org.springframework.http.client.reactive.JettyClientHttpConnector;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Builder for {@link ClientHttpConnectorBuilder#jetty()}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public final class JettyClientHttpConnectorBuilder
|
||||
extends AbstractClientHttpConnectorBuilder<JettyClientHttpConnector> {
|
||||
|
||||
private final JettyHttpClientBuilder httpClientBuilder;
|
||||
|
||||
JettyClientHttpConnectorBuilder() {
|
||||
this(null, new JettyHttpClientBuilder());
|
||||
}
|
||||
|
||||
private JettyClientHttpConnectorBuilder(List<Consumer<JettyClientHttpConnector>> customizers,
|
||||
JettyHttpClientBuilder httpClientBuilder) {
|
||||
super(customizers);
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JettyClientHttpConnectorBuilder withCustomizer(Consumer<JettyClientHttpConnector> customizer) {
|
||||
return new JettyClientHttpConnectorBuilder(mergedCustomizers(customizer), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JettyClientHttpConnectorBuilder withCustomizers(Collection<Consumer<JettyClientHttpConnector>> customizers) {
|
||||
return new JettyClientHttpConnectorBuilder(mergedCustomizers(customizers), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JettyClientHttpConnectorBuilder} that applies additional
|
||||
* customization to the underlying {@link HttpClient}.
|
||||
* @param httpClientCustomizer the customizer to apply
|
||||
* @return a new {@link JettyClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
public JettyClientHttpConnectorBuilder withHttpClientCustomizer(Consumer<HttpClient> httpClientCustomizer) {
|
||||
Assert.notNull(httpClientCustomizer, "'httpClientCustomizer' must not be null");
|
||||
return new JettyClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JettyClientHttpConnectorBuilder} that applies additional
|
||||
* customization to the underlying {@link HttpClientTransport}.
|
||||
* @param httpClientTransportCustomizer the customizer to apply
|
||||
* @return a new {@link JettyClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
public JettyClientHttpConnectorBuilder withHttpClientTransportCustomizer(
|
||||
Consumer<HttpClientTransport> httpClientTransportCustomizer) {
|
||||
Assert.notNull(httpClientTransportCustomizer, "'httpClientTransportCustomizer' must not be null");
|
||||
return new JettyClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withHttpClientTransportCustomizer(httpClientTransportCustomizer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link JettyClientHttpConnectorBuilder} that applies additional
|
||||
* customization to the underlying {@link ClientConnector}.
|
||||
* @param clientConnectorCustomizerCustomizer the customizer to apply
|
||||
* @return a new {@link JettyClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
public JettyClientHttpConnectorBuilder withClientConnectorCustomizerCustomizer(
|
||||
Consumer<ClientConnector> clientConnectorCustomizerCustomizer) {
|
||||
Assert.notNull(clientConnectorCustomizerCustomizer, "'clientConnectorCustomizerCustomizer' must not be null");
|
||||
return new JettyClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withClientConnectorCustomizerCustomizer(clientConnectorCustomizerCustomizer));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JettyClientHttpConnector createClientHttpConnector(ClientHttpConnectorSettings settings) {
|
||||
HttpClient httpClient = this.httpClientBuilder.build(asHttpClientSettings(settings));
|
||||
return new JettyClientHttpConnector(httpClient);
|
||||
}
|
||||
|
||||
static class Classes {
|
||||
|
||||
static final String HTTP_CLIENT = "org.eclipse.jetty.client.HttpClient";
|
||||
|
||||
static final String REACTIVE_REQUEST = "org.eclipse.jetty.reactive.client.ReactiveRequest";
|
||||
|
||||
static boolean present(ClassLoader classLoader) {
|
||||
return ClassUtils.isPresent(HTTP_CLIENT, classLoader)
|
||||
&& ClassUtils.isPresent(REACTIVE_REQUEST, classLoader);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.http.client.reactive;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
|
||||
import org.springframework.boot.http.client.ReactorHttpClientBuilder;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Builder for {@link ClientHttpConnectorBuilder#reactor()}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public final class ReactorClientHttpConnectorBuilder
|
||||
extends AbstractClientHttpConnectorBuilder<ReactorClientHttpConnector> {
|
||||
|
||||
private final ReactorHttpClientBuilder httpClientBuilder;
|
||||
|
||||
ReactorClientHttpConnectorBuilder() {
|
||||
this(null, new ReactorHttpClientBuilder());
|
||||
}
|
||||
|
||||
private ReactorClientHttpConnectorBuilder(List<Consumer<ReactorClientHttpConnector>> customizers,
|
||||
ReactorHttpClientBuilder httpClientBuilder) {
|
||||
super(customizers);
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactorClientHttpConnectorBuilder withCustomizer(Consumer<ReactorClientHttpConnector> customizer) {
|
||||
return new ReactorClientHttpConnectorBuilder(mergedCustomizers(customizer), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactorClientHttpConnectorBuilder withCustomizers(
|
||||
Collection<Consumer<ReactorClientHttpConnector>> customizers) {
|
||||
return new ReactorClientHttpConnectorBuilder(mergedCustomizers(customizers), this.httpClientBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ReactorClientHttpConnectorBuilder} that applies additional
|
||||
* customization to the underlying {@link HttpClient}.
|
||||
* @param httpClientCustomizer the customizer to apply
|
||||
* @return a new {@link ReactorClientHttpConnectorBuilder} instance
|
||||
*/
|
||||
public ReactorClientHttpConnectorBuilder withHttpClientCustomizer(UnaryOperator<HttpClient> httpClientCustomizer) {
|
||||
Assert.notNull(httpClientCustomizer, "'httpClientCustomizer' must not be null");
|
||||
return new ReactorClientHttpConnectorBuilder(getCustomizers(),
|
||||
this.httpClientBuilder.withHttpClientCustomizer(httpClientCustomizer));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ReactorClientHttpConnector createClientHttpConnector(ClientHttpConnectorSettings settings) {
|
||||
HttpClient httpClient = this.httpClientBuilder.build(asHttpClientSettings(settings));
|
||||
return new ReactorClientHttpConnector(httpClient);
|
||||
}
|
||||
|
||||
static class Classes {
|
||||
|
||||
static final String HTTP_CLIENT = "reactor.netty.http.client.HttpClient";
|
||||
|
||||
static boolean present(ClassLoader classLoader) {
|
||||
return ClassUtils.isPresent(HTTP_CLIENT, classLoader);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Client-side reactive HTTP support classes.
|
||||
*/
|
||||
package org.springframework.boot.http.client.reactive;
|
||||
@@ -38,7 +38,8 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpComponentsClientHttpRequestFactoryBuilder}.
|
||||
* Tests for {@link HttpComponentsClientHttpRequestFactoryBuilder} and
|
||||
* {@link HttpComponentsHttpClientBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -25,7 +25,7 @@ import org.springframework.http.client.JdkClientHttpRequestFactory;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
/**
|
||||
* Tests for {@link JdkClientHttpRequestFactoryBuilder}.
|
||||
* Tests for {@link JdkClientHttpRequestFactoryBuilder} and {@link JdkHttpClientBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -25,7 +25,8 @@ import org.springframework.http.client.JettyClientHttpRequestFactory;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
/**
|
||||
* Tests for {@link JettyClientHttpRequestFactoryBuilder}.
|
||||
* Tests for {@link JettyClientHttpRequestFactoryBuilder} and
|
||||
* {@link JettyHttpClientBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -31,7 +31,8 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReactorClientHttpRequestFactoryBuilder}.
|
||||
* Tests for {@link ReactorClientHttpRequestFactoryBuilder} and
|
||||
* {@link ReactorHttpClientBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
|
||||
@@ -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.http.client.reactive;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.time.Duration;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.boot.http.client.HttpRedirects;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundleKey;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories;
|
||||
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.Ssl.ClientAuth;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFunctions;
|
||||
import org.springframework.web.reactive.function.client.WebClientRequestException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Base class for {@link ClientHttpConnectorBuilder} tests.
|
||||
*
|
||||
* @param <T> The {@link ClientHttpConnector} type
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@DirtiesUrlFactories
|
||||
abstract class AbstractClientHttpConnectorBuilderTests<T extends ClientHttpConnector> {
|
||||
|
||||
private static final Function<HttpMethod, HttpStatus> ALWAYS_FOUND = (method) -> HttpStatus.FOUND;
|
||||
|
||||
private final Class<T> connectorType;
|
||||
|
||||
private final ClientHttpConnectorBuilder<T> builder;
|
||||
|
||||
AbstractClientHttpConnectorBuilderTests(Class<T> connectorType, ClientHttpConnectorBuilder<T> builder) {
|
||||
this.connectorType = connectorType;
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildReturnsConnectorOfExpectedType() {
|
||||
T connector = this.builder.build();
|
||||
assertThat(connector).isInstanceOf(this.connectorType);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWhenHasConnectTimeout() {
|
||||
ClientHttpConnectorSettings settings = ClientHttpConnectorSettings.defaults()
|
||||
.withConnectTimeout(Duration.ofSeconds(60));
|
||||
T connector = this.builder.build(settings);
|
||||
assertThat(connectTimeout(connector)).isEqualTo(Duration.ofSeconds(60).toMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWhenHadReadTimeout() {
|
||||
ClientHttpConnectorSettings settings = ClientHttpConnectorSettings.defaults()
|
||||
.withReadTimeout(Duration.ofSeconds(120));
|
||||
T connector = this.builder.build(settings);
|
||||
assertThat(readTimeout(connector)).isEqualTo(Duration.ofSeconds(120).toMillis());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@WithPackageResources("test.jks")
|
||||
@ValueSource(strings = { "GET", "POST" })
|
||||
void connectWithSslBundle(String httpMethod) throws Exception {
|
||||
TomcatServletWebServerFactory webServerFactory = new TomcatServletWebServerFactory(0);
|
||||
webServerFactory.setSsl(ssl());
|
||||
WebServer webServer = webServerFactory
|
||||
.getWebServer((context) -> context.addServlet("test", TestServlet.class).addMapping("/"));
|
||||
try {
|
||||
webServer.start();
|
||||
int port = webServer.getPort();
|
||||
URI uri = new URI("https://localhost:%s".formatted(port));
|
||||
ClientHttpConnector insecureConnector = this.builder.build();
|
||||
ClientRequest insecureRequest = createRequest(httpMethod, uri);
|
||||
assertThatExceptionOfType(WebClientRequestException.class)
|
||||
.isThrownBy(() -> getResponse(insecureConnector, insecureRequest))
|
||||
.withCauseInstanceOf(SSLHandshakeException.class);
|
||||
ClientHttpConnector secureConnector = this.builder
|
||||
.build(ClientHttpConnectorSettings.ofSslBundle(sslBundle()));
|
||||
ClientRequest secureRequest = createRequest(httpMethod, uri);
|
||||
ClientResponse secureResponse = getResponse(secureConnector, secureRequest);
|
||||
assertThat(secureResponse.bodyToMono(String.class).block())
|
||||
.contains("Received " + httpMethod + " request to /");
|
||||
}
|
||||
finally {
|
||||
webServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@WithPackageResources("test.jks")
|
||||
@ValueSource(strings = { "GET", "POST" })
|
||||
void connectWithSslBundleAndOptionsMismatch(String httpMethod) throws Exception {
|
||||
TomcatServletWebServerFactory webServerFactory = new TomcatServletWebServerFactory(0);
|
||||
webServerFactory.setSsl(ssl("TLS_AES_128_GCM_SHA256"));
|
||||
WebServer webServer = webServerFactory
|
||||
.getWebServer((context) -> context.addServlet("test", TestServlet.class).addMapping("/"));
|
||||
try {
|
||||
webServer.start();
|
||||
int port = webServer.getPort();
|
||||
URI uri = new URI("https://localhost:%s".formatted(port));
|
||||
ClientHttpConnector secureConnector = this.builder.build(ClientHttpConnectorSettings
|
||||
.ofSslBundle(sslBundle(SslOptions.of(Set.of("TLS_AES_256_GCM_SHA384"), null))));
|
||||
ClientRequest secureRequest = createRequest(httpMethod, uri);
|
||||
assertThatExceptionOfType(WebClientRequestException.class)
|
||||
.isThrownBy(() -> getResponse(secureConnector, secureRequest))
|
||||
.withCauseInstanceOf(SSLHandshakeException.class);
|
||||
}
|
||||
finally {
|
||||
webServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "GET", "POST", "PUT", "PATCH", "DELETE" })
|
||||
void redirectDefault(String httpMethod) throws Exception {
|
||||
testRedirect(null, HttpMethod.valueOf(httpMethod), this::getExpectedRedirect);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "GET", "POST", "PUT", "PATCH", "DELETE" })
|
||||
void redirectFollow(String httpMethod) throws Exception {
|
||||
ClientHttpConnectorSettings settings = ClientHttpConnectorSettings.defaults()
|
||||
.withRedirects(HttpRedirects.FOLLOW);
|
||||
testRedirect(settings, HttpMethod.valueOf(httpMethod), this::getExpectedRedirect);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "GET", "POST", "PUT", "PATCH", "DELETE" })
|
||||
void redirectDontFollow(String httpMethod) throws Exception {
|
||||
ClientHttpConnectorSettings settings = ClientHttpConnectorSettings.defaults()
|
||||
.withRedirects(HttpRedirects.DONT_FOLLOW);
|
||||
testRedirect(settings, HttpMethod.valueOf(httpMethod), ALWAYS_FOUND);
|
||||
}
|
||||
|
||||
protected final void testRedirect(ClientHttpConnectorSettings settings, HttpMethod httpMethod,
|
||||
Function<HttpMethod, HttpStatus> expectedStatusForMethod) throws URISyntaxException {
|
||||
HttpStatus expectedStatus = expectedStatusForMethod.apply(httpMethod);
|
||||
TomcatServletWebServerFactory webServerFactory = new TomcatServletWebServerFactory(0);
|
||||
WebServer webServer = webServerFactory
|
||||
.getWebServer((context) -> context.addServlet("test", TestServlet.class).addMapping("/"));
|
||||
try {
|
||||
webServer.start();
|
||||
int port = webServer.getPort();
|
||||
URI uri = new URI("http://localhost:%s".formatted(port) + "/redirect");
|
||||
ClientHttpConnector connector = this.builder.build(settings);
|
||||
ClientRequest request = createRequest(httpMethod, uri);
|
||||
ClientResponse response = getResponse(connector, request);
|
||||
assertThat(response.statusCode()).isEqualTo(expectedStatus);
|
||||
if (expectedStatus == HttpStatus.OK) {
|
||||
assertThat(response.bodyToMono(String.class).block()).contains("request to /redirected");
|
||||
}
|
||||
}
|
||||
finally {
|
||||
webServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private ClientRequest createRequest(String httpMethod, URI uri) {
|
||||
return createRequest(HttpMethod.valueOf(httpMethod), uri);
|
||||
}
|
||||
|
||||
private ClientRequest createRequest(HttpMethod httpMethod, URI uri) {
|
||||
return ClientRequest.create(httpMethod, uri).build();
|
||||
}
|
||||
|
||||
private ClientResponse getResponse(ClientHttpConnector connector, ClientRequest request) {
|
||||
return ExchangeFunctions.create(connector).exchange(request).block();
|
||||
}
|
||||
|
||||
private Ssl ssl(String... ciphers) {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setClientAuth(ClientAuth.NEED);
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setKeyStore("classpath:test.jks");
|
||||
ssl.setTrustStore("classpath:test.jks");
|
||||
if (ciphers.length > 0) {
|
||||
ssl.setCiphers(ciphers);
|
||||
}
|
||||
return ssl;
|
||||
}
|
||||
|
||||
protected final SslBundle sslBundle() {
|
||||
return sslBundle(SslOptions.NONE);
|
||||
}
|
||||
|
||||
protected final SslBundle sslBundle(SslOptions sslOptions) {
|
||||
JksSslStoreDetails storeDetails = JksSslStoreDetails.forLocation("classpath:test.jks");
|
||||
JksSslStoreBundle stores = new JksSslStoreBundle(storeDetails, storeDetails);
|
||||
return SslBundle.of(stores, SslBundleKey.of("password"), sslOptions);
|
||||
}
|
||||
|
||||
protected HttpStatus getExpectedRedirect(HttpMethod httpMethod) {
|
||||
return HttpStatus.OK;
|
||||
}
|
||||
|
||||
protected abstract long connectTimeout(T connector);
|
||||
|
||||
protected abstract long readTimeout(T connector);
|
||||
|
||||
public static class TestServlet extends HttpServlet {
|
||||
|
||||
@Override
|
||||
public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
|
||||
if ("/redirect".equals(req.getRequestURI())) {
|
||||
res.sendRedirect("/redirected");
|
||||
return;
|
||||
}
|
||||
res.getWriter().println("Received " + req.getMethod() + " request to " + req.getRequestURI());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* 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.http.client.reactive;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.http.client.reactive.ClientHttpResponse;
|
||||
import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.JdkClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.JettyClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link ClientHttpConnectorBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ClientHttpConnectorBuilderTests {
|
||||
|
||||
@Test
|
||||
void withCustomizerAppliesCustomizers() {
|
||||
ClientHttpConnectorBuilder<JdkClientHttpConnector> builder = (settings) -> new JdkClientHttpConnector();
|
||||
builder = builder.withCustomizer(this::setJdkReadTimeout);
|
||||
JdkClientHttpConnector connector = builder.build(null);
|
||||
assertThat(connector).extracting("readTimeout").isEqualTo(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void withCustomizersAppliesCustomizers() {
|
||||
ClientHttpConnectorBuilder<JdkClientHttpConnector> builder = (settings) -> new JdkClientHttpConnector();
|
||||
builder = builder.withCustomizers(List.of(this::setJdkReadTimeout));
|
||||
JdkClientHttpConnector connector = builder.build(null);
|
||||
assertThat(connector).extracting("readTimeout").isEqualTo(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void reactorReturnsReactorFactoryBuilder() {
|
||||
assertThat(ClientHttpConnectorBuilder.reactor()).isInstanceOf(ReactorClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void jettyReturnsJettyFactoryBuilder() {
|
||||
assertThat(ClientHttpConnectorBuilder.jetty()).isInstanceOf(JettyClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void httpComponentsReturnsHttpComponentsFactoryBuilder() {
|
||||
assertThat(ClientHttpConnectorBuilder.httpComponents())
|
||||
.isInstanceOf(HttpComponentsClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void jdkReturnsJdkFactoryBuilder() {
|
||||
assertThat(ClientHttpConnectorBuilder.jdk()).isInstanceOf(JdkClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenExactlyClientHttpRequestFactoryTypeThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> ClientHttpConnectorBuilder.of(ClientHttpConnector.class))
|
||||
.withMessage("'clientHttpConnectorType' must be an implementation of ClientHttpConnector");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenReactorFactoryReturnsReactorFactoryBuilder() {
|
||||
assertThat(ClientHttpConnectorBuilder.of(ReactorClientHttpConnector.class))
|
||||
.isInstanceOf(ReactorClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenJettyFactoryReturnsReactorFactoryBuilder() {
|
||||
assertThat(ClientHttpConnectorBuilder.of(JettyClientHttpConnector.class))
|
||||
.isInstanceOf(JettyClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenHttpComponentsFactoryReturnsHttpComponentsFactoryBuilder() {
|
||||
assertThat(ClientHttpConnectorBuilder.of(HttpComponentsClientHttpConnector.class))
|
||||
.isInstanceOf(HttpComponentsClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenJdkFactoryReturnsJdkFactoryBuilder() {
|
||||
assertThat(ClientHttpConnectorBuilder.of(JdkClientHttpConnector.class))
|
||||
.isInstanceOf(JdkClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenUnknownTypeThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ClientHttpConnectorBuilder.of(TestClientHttpConnector.class))
|
||||
.withMessage("'clientHttpConnectorType' " + TestClientHttpConnector.class.getName() + " is not supported");
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectWhenReactor() {
|
||||
assertThat(ClientHttpConnectorBuilder.detect()).isInstanceOf(ReactorClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ClassPathExclusions({ "reactor-netty-http-*.jar" })
|
||||
void detectWhenJetty() {
|
||||
assertThat(ClientHttpConnectorBuilder.detect()).isInstanceOf(JettyClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ClassPathExclusions({ "reactor-netty-http-*.jar", "jetty-client-*.jar" })
|
||||
void detectWhenHttpComponents() {
|
||||
assertThat(ClientHttpConnectorBuilder.detect()).isInstanceOf(HttpComponentsClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ClassPathExclusions({ "reactor-netty-http-*.jar", "jetty-client-*.jar", "httpclient5-*.jar" })
|
||||
void detectWhenJdk() {
|
||||
assertThat(ClientHttpConnectorBuilder.detect()).isInstanceOf(JdkClientHttpConnectorBuilder.class);
|
||||
}
|
||||
|
||||
private void setJdkReadTimeout(JdkClientHttpConnector factory) {
|
||||
factory.setReadTimeout(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
public static class TestClientHttpConnector implements ClientHttpConnector {
|
||||
|
||||
@Override
|
||||
public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
|
||||
Function<? super ClientHttpRequest, Mono<Void>> requestCallback) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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.http.client.reactive;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.hc.client5.http.HttpRoute;
|
||||
import org.apache.hc.client5.http.async.HttpAsyncClient;
|
||||
import org.apache.hc.client5.http.config.ConnectionConfig;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
|
||||
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
|
||||
import org.apache.hc.core5.function.Resolver;
|
||||
import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.http.client.HttpComponentsHttpAsyncClientBuilder;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpComponentsClientHttpConnectorBuilder} and
|
||||
* {@link HttpComponentsHttpAsyncClientBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class HttpComponentsClientHttpConnectorBuilderTests
|
||||
extends AbstractClientHttpConnectorBuilderTests<HttpComponentsClientHttpConnector> {
|
||||
|
||||
HttpComponentsClientHttpConnectorBuilderTests() {
|
||||
super(HttpComponentsClientHttpConnector.class, ClientHttpConnectorBuilder.httpComponents());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withCustomizers() {
|
||||
TestCustomizer<HttpAsyncClientBuilder> httpClientCustomizer1 = new TestCustomizer<>();
|
||||
TestCustomizer<HttpAsyncClientBuilder> httpClientCustomizer2 = new TestCustomizer<>();
|
||||
TestCustomizer<PoolingAsyncClientConnectionManagerBuilder> connectionManagerCustomizer = new TestCustomizer<>();
|
||||
TestCustomizer<ConnectionConfig.Builder> connectionConfigCustomizer1 = new TestCustomizer<>();
|
||||
TestCustomizer<ConnectionConfig.Builder> connectionConfigCustomizer2 = new TestCustomizer<>();
|
||||
TestCustomizer<RequestConfig.Builder> defaultRequestConfigCustomizer = new TestCustomizer<>();
|
||||
TestCustomizer<RequestConfig.Builder> defaultRequestConfigCustomizer1 = new TestCustomizer<>();
|
||||
ClientHttpConnectorBuilder.httpComponents()
|
||||
.withHttpClientCustomizer(httpClientCustomizer1)
|
||||
.withHttpClientCustomizer(httpClientCustomizer2)
|
||||
.withConnectionManagerCustomizer(connectionManagerCustomizer)
|
||||
.withConnectionConfigCustomizer(connectionConfigCustomizer1)
|
||||
.withConnectionConfigCustomizer(connectionConfigCustomizer2)
|
||||
.withDefaultRequestConfigCustomizer(defaultRequestConfigCustomizer)
|
||||
.withDefaultRequestConfigCustomizer(defaultRequestConfigCustomizer1)
|
||||
.build();
|
||||
httpClientCustomizer1.assertCalled();
|
||||
httpClientCustomizer2.assertCalled();
|
||||
connectionManagerCustomizer.assertCalled();
|
||||
connectionConfigCustomizer1.assertCalled();
|
||||
connectionConfigCustomizer2.assertCalled();
|
||||
defaultRequestConfigCustomizer.assertCalled();
|
||||
defaultRequestConfigCustomizer1.assertCalled();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void withTlsSocketStrategyFactory() {
|
||||
ClientHttpConnectorSettings settings = ClientHttpConnectorSettings.ofSslBundle(sslBundle());
|
||||
List<SslBundle> bundles = new ArrayList<>();
|
||||
Function<SslBundle, TlsStrategy> tlsSocketStrategyFactory = (bundle) -> {
|
||||
bundles.add(bundle);
|
||||
return (sessionLayer, host, localAddress, remoteAddress, attachment, handshakeTimeout) -> false;
|
||||
};
|
||||
ClientHttpConnectorBuilder.httpComponents()
|
||||
.withTlsSocketStrategyFactory(tlsSocketStrategyFactory)
|
||||
.build(settings);
|
||||
assertThat(bundles).contains(settings.sslBundle());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long connectTimeout(HttpComponentsClientHttpConnector connector) {
|
||||
return getConnectorConfig(connector).getConnectTimeout().toMilliseconds();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long readTimeout(HttpComponentsClientHttpConnector connector) {
|
||||
return getConnectorConfig(connector).getSocketTimeout().toMilliseconds();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ConnectionConfig getConnectorConfig(HttpComponentsClientHttpConnector connector) {
|
||||
HttpAsyncClient httpClient = (HttpAsyncClient) ReflectionTestUtils.getField(connector, "client");
|
||||
Object manager = ReflectionTestUtils.getField(httpClient, "manager");
|
||||
ConnectionConfig connectorConfig = ((Resolver<HttpRoute, ConnectionConfig>) ReflectionTestUtils
|
||||
.getField(manager, "connectionConfigResolver")).resolve(null);
|
||||
return connectorConfig;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.http.client.reactive;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.JdkHttpClientBuilder;
|
||||
import org.springframework.http.client.reactive.JdkClientHttpConnector;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
/**
|
||||
* Tests for {@link JdkClientHttpConnectorBuilder} and {@link JdkHttpClientBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class JdkClientHttpConnectorBuilderTests extends AbstractClientHttpConnectorBuilderTests<JdkClientHttpConnector> {
|
||||
|
||||
JdkClientHttpConnectorBuilderTests() {
|
||||
super(JdkClientHttpConnector.class, ClientHttpConnectorBuilder.jdk());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withCustomizers() {
|
||||
TestCustomizer<HttpClient.Builder> httpClientCustomizer1 = new TestCustomizer<>();
|
||||
TestCustomizer<HttpClient.Builder> httpClientCustomizer2 = new TestCustomizer<>();
|
||||
ClientHttpRequestFactoryBuilder.jdk()
|
||||
.withHttpClientCustomizer(httpClientCustomizer1)
|
||||
.withHttpClientCustomizer(httpClientCustomizer2)
|
||||
.build();
|
||||
httpClientCustomizer1.assertCalled();
|
||||
httpClientCustomizer2.assertCalled();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long connectTimeout(JdkClientHttpConnector connector) {
|
||||
HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(connector, "httpClient");
|
||||
return httpClient.connectTimeout().get().toMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long readTimeout(JdkClientHttpConnector connector) {
|
||||
Duration readTimeout = (Duration) ReflectionTestUtils.getField(connector, "readTimeout");
|
||||
return readTimeout.toMillis();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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.http.client.reactive;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpClientTransport;
|
||||
import org.eclipse.jetty.io.ClientConnector;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.JettyHttpClientBuilder;
|
||||
import org.springframework.http.client.reactive.JettyClientHttpConnector;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
/**
|
||||
* Tests for {@link JettyClientHttpConnectorBuilder} and {@link JettyHttpClientBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class JettyClientHttpConnectorBuilderTests extends AbstractClientHttpConnectorBuilderTests<JettyClientHttpConnector> {
|
||||
|
||||
JettyClientHttpConnectorBuilderTests() {
|
||||
super(JettyClientHttpConnector.class, ClientHttpConnectorBuilder.jetty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withCustomizers() {
|
||||
TestCustomizer<HttpClient> httpClientCustomizer1 = new TestCustomizer<>();
|
||||
TestCustomizer<HttpClient> httpClientCustomizer2 = new TestCustomizer<>();
|
||||
TestCustomizer<HttpClientTransport> httpClientTransportCustomizer = new TestCustomizer<>();
|
||||
TestCustomizer<ClientConnector> clientConnectorCustomizerCustomizer = new TestCustomizer<>();
|
||||
ClientHttpRequestFactoryBuilder.jetty()
|
||||
.withHttpClientCustomizer(httpClientCustomizer1)
|
||||
.withHttpClientCustomizer(httpClientCustomizer2)
|
||||
.withHttpClientTransportCustomizer(httpClientTransportCustomizer)
|
||||
.withClientConnectorCustomizerCustomizer(clientConnectorCustomizerCustomizer)
|
||||
.build();
|
||||
httpClientCustomizer1.assertCalled();
|
||||
httpClientCustomizer2.assertCalled();
|
||||
httpClientTransportCustomizer.assertCalled();
|
||||
clientConnectorCustomizerCustomizer.assertCalled();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long connectTimeout(JettyClientHttpConnector connector) {
|
||||
return ((HttpClient) ReflectionTestUtils.getField(connector, "httpClient")).getConnectTimeout();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long readTimeout(JettyClientHttpConnector connector) {
|
||||
HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(connector, "httpClient");
|
||||
return ((Duration) ReflectionTestUtils.getField(httpClient, "readTimeout")).toMillis();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.http.client.reactive;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import io.netty.channel.ChannelOption;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
|
||||
import org.springframework.boot.http.client.ReactorHttpClientBuilder;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReactorClientHttpConnectorBuilder} and
|
||||
* {@link ReactorHttpClientBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ReactorClientHttpConnectorBuilderTests
|
||||
extends AbstractClientHttpConnectorBuilderTests<ReactorClientHttpConnector> {
|
||||
|
||||
ReactorClientHttpConnectorBuilderTests() {
|
||||
super(ReactorClientHttpConnector.class, ClientHttpConnectorBuilder.reactor());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withCustomizers() {
|
||||
List<HttpClient> httpClients = new ArrayList<>();
|
||||
UnaryOperator<HttpClient> httpClientCustomizer1 = (httpClient) -> {
|
||||
httpClients.add(httpClient);
|
||||
return httpClient;
|
||||
};
|
||||
UnaryOperator<HttpClient> httpClientCustomizer2 = (httpClient) -> {
|
||||
httpClients.add(httpClient);
|
||||
return httpClient;
|
||||
};
|
||||
ClientHttpRequestFactoryBuilder.reactor()
|
||||
.withHttpClientCustomizer(httpClientCustomizer1)
|
||||
.withHttpClientCustomizer(httpClientCustomizer2)
|
||||
.build();
|
||||
assertThat(httpClients).hasSize(2);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long connectTimeout(ReactorClientHttpConnector connector) {
|
||||
return (int) ((HttpClient) ReflectionTestUtils.getField(connector, "httpClient")).configuration()
|
||||
.options()
|
||||
.get(ChannelOption.CONNECT_TIMEOUT_MILLIS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long readTimeout(ReactorClientHttpConnector connector) {
|
||||
return (int) ((HttpClient) ReflectionTestUtils.getField(connector, "httpClient")).configuration()
|
||||
.responseTimeout()
|
||||
.toMillis();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -14,24 +14,29 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.web.reactive.function.client;
|
||||
package org.springframework.boot.http.client.reactive;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Internal factory used to create {@link ClientHttpConnector} instances.
|
||||
* Test customizer that can assert that it has been called.
|
||||
*
|
||||
* @param <T> the {@link ClientHttpConnector} type
|
||||
* @param <T> type being customized
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface ClientHttpConnectorFactory<T extends ClientHttpConnector> {
|
||||
class TestCustomizer<T> implements Consumer<T> {
|
||||
|
||||
default T createClientHttpConnector() {
|
||||
return createClientHttpConnector(null);
|
||||
private boolean called;
|
||||
|
||||
@Override
|
||||
public void accept(T t) {
|
||||
this.called = true;
|
||||
}
|
||||
|
||||
T createClientHttpConnector(SslBundle sslBundle);
|
||||
void assertCalled() {
|
||||
assertThat(this.called).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user