Add support for Reactor Netty to ClientHttpRequestFactories
Closes gh-42587
This commit is contained in:
@@ -194,6 +194,7 @@ In order of preference, the following clients are supported:
|
||||
|
||||
. Apache HttpClient
|
||||
. Jetty HttpClient
|
||||
. Reactor Netty HttpClient
|
||||
. OkHttp (deprecated)
|
||||
. Simple JDK client (`HttpURLConnection`)
|
||||
|
||||
|
||||
@@ -27,10 +27,12 @@ import java.util.function.Supplier;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import io.netty.handler.ssl.SslContextBuilder;
|
||||
import okhttp3.OkHttpClient;
|
||||
import org.apache.hc.client5.http.classic.HttpClient;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
|
||||
@@ -42,19 +44,23 @@ import org.apache.hc.core5.http.io.SocketConfig;
|
||||
import org.eclipse.jetty.client.transport.HttpClientTransportDynamic;
|
||||
import org.eclipse.jetty.io.ClientConnector;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
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.http.client.AbstractClientHttpRequestFactoryWrapper;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.http.client.JdkClientHttpRequestFactory;
|
||||
import org.springframework.http.client.JettyClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ReactorClientHttpRequestFactory;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.function.ThrowingConsumer;
|
||||
|
||||
/**
|
||||
* Utility class that can be used to create {@link ClientHttpRequestFactory} instances
|
||||
@@ -79,6 +85,10 @@ public final class ClientHttpRequestFactories {
|
||||
|
||||
private static final boolean JETTY_CLIENT_PRESENT = ClassUtils.isPresent(JETTY_CLIENT_CLASS, null);
|
||||
|
||||
static final String REACTOR_CLIENT_CLASS = "reactor.netty.http.client.HttpClient";
|
||||
|
||||
private static final boolean REACTOR_CLIENT_PRESENT = ClassUtils.isPresent(REACTOR_CLIENT_CLASS, null);
|
||||
|
||||
private ClientHttpRequestFactories() {
|
||||
}
|
||||
|
||||
@@ -89,6 +99,7 @@ public final class ClientHttpRequestFactories {
|
||||
* <ol>
|
||||
* <li>{@link HttpComponentsClientHttpRequestFactory}</li>
|
||||
* <li>{@link JettyClientHttpRequestFactory}</li>
|
||||
* <li>{@link ReactorClientHttpRequestFactory}</li>
|
||||
* <li>{@link org.springframework.http.client.OkHttp3ClientHttpRequestFactory
|
||||
* OkHttp3ClientHttpRequestFactory} (deprecated)</li>
|
||||
* <li>{@link SimpleClientHttpRequestFactory}</li>
|
||||
@@ -105,6 +116,9 @@ public final class ClientHttpRequestFactories {
|
||||
if (JETTY_CLIENT_PRESENT) {
|
||||
return Jetty.get(settings);
|
||||
}
|
||||
if (REACTOR_CLIENT_PRESENT) {
|
||||
return Reactor.get(settings);
|
||||
}
|
||||
if (OKHTTP_CLIENT_PRESENT) {
|
||||
return OkHttp.get(settings);
|
||||
}
|
||||
@@ -120,6 +134,7 @@ public final class ClientHttpRequestFactories {
|
||||
* <li>{@link HttpComponentsClientHttpRequestFactory}</li>
|
||||
* <li>{@link JdkClientHttpRequestFactory}</li>
|
||||
* <li>{@link JettyClientHttpRequestFactory}</li>
|
||||
* <li>{@link ReactorClientHttpRequestFactory}</li>
|
||||
* <li>{@link org.springframework.http.client.OkHttp3ClientHttpRequestFactory
|
||||
* OkHttp3ClientHttpRequestFactory} (deprecated)</li>
|
||||
* <li>{@link SimpleClientHttpRequestFactory}</li>
|
||||
@@ -144,6 +159,9 @@ public final class ClientHttpRequestFactories {
|
||||
if (requestFactoryType == JettyClientHttpRequestFactory.class) {
|
||||
return (T) Jetty.get(settings);
|
||||
}
|
||||
if (requestFactoryType == ReactorClientHttpRequestFactory.class) {
|
||||
return (T) Reactor.get(settings);
|
||||
}
|
||||
if (requestFactoryType == JdkClientHttpRequestFactory.class) {
|
||||
return (T) Jdk.get(settings);
|
||||
}
|
||||
@@ -286,6 +304,41 @@ public final class ClientHttpRequestFactories {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Support for {@link ReactorClientHttpRequestFactory}.
|
||||
*/
|
||||
static class Reactor {
|
||||
|
||||
static ReactorClientHttpRequestFactory get(ClientHttpRequestFactorySettings settings) {
|
||||
ReactorClientHttpRequestFactory requestFactory = createRequestFactory(settings.sslBundle());
|
||||
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 static ReactorClientHttpRequestFactory createRequestFactory(SslBundle sslBundle) {
|
||||
if (sslBundle != null) {
|
||||
reactor.netty.http.client.HttpClient httpClient = reactor.netty.http.client.HttpClient.create()
|
||||
.secure((ThrowingConsumer.of((spec) -> configureSsl(spec, sslBundle))));
|
||||
return new ReactorClientHttpRequestFactory(httpClient);
|
||||
}
|
||||
return new ReactorClientHttpRequestFactory();
|
||||
}
|
||||
|
||||
private static 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Support for {@link JdkClientHttpRequestFactory}.
|
||||
*/
|
||||
|
||||
@@ -36,7 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* @deprecated since 3.2.0 for removal in 3.4.0
|
||||
*/
|
||||
@ClassPathOverrides("com.squareup.okhttp3:okhttp:3.14.9")
|
||||
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar" })
|
||||
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar", "reactor-netty-http-*.jar" })
|
||||
@Deprecated(since = "3.2.0", forRemoval = true)
|
||||
@SuppressWarnings("removal")
|
||||
class ClientHttpRequestFactoriesOkHttp3Tests
|
||||
|
||||
@@ -34,7 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* @author Andy Wilkinson
|
||||
* @deprecated since 3.2.0 for removal in 3.4.0
|
||||
*/
|
||||
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar" })
|
||||
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar", "reactor-netty-http-*.jar" })
|
||||
@Deprecated(since = "3.2.0", forRemoval = true)
|
||||
@SuppressWarnings("removal")
|
||||
class ClientHttpRequestFactoriesOkHttp4Tests
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.web.client;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import io.netty.channel.ChannelOption;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
|
||||
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
|
||||
import org.springframework.http.client.ReactorClientHttpRequestFactory;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
/**
|
||||
* Tests for {@link ClientHttpRequestFactories} when Reactor Netty is the predominant HTTP
|
||||
* client.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar" })
|
||||
class ClientHttpRequestFactoriesReactorTests
|
||||
extends AbstractClientHttpRequestFactoriesTests<ReactorClientHttpRequestFactory> {
|
||||
|
||||
ClientHttpRequestFactoriesReactorTests() {
|
||||
super(ReactorClientHttpRequestFactory.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long connectTimeout(ReactorClientHttpRequestFactory requestFactory) {
|
||||
return (int) ((HttpClient) ReflectionTestUtils.getField(requestFactory, "httpClient")).configuration()
|
||||
.options()
|
||||
.get(ChannelOption.CONNECT_TIMEOUT_MILLIS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long readTimeout(ReactorClientHttpRequestFactory requestFactory) {
|
||||
return ((Duration) ReflectionTestUtils.getField(requestFactory, "readTimeout")).toMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supportsSettingConnectTimeout() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supportsSettingReadTimeout() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar", "okhttp-*.jar" })
|
||||
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar", "okhttp-*.jar", "reactor-netty-http-*.jar" })
|
||||
class ClientHttpRequestFactoriesSimpleTests
|
||||
extends AbstractClientHttpRequestFactoriesTests<SimpleClientHttpRequestFactory> {
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.springframework.http.client.ClientHttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.http.client.JdkClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ReactorClientHttpRequestFactory;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -67,6 +68,13 @@ class ClientHttpRequestFactoriesTests {
|
||||
assertThat(requestFactory).isInstanceOf(HttpComponentsClientHttpRequestFactory.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOfReactorFactoryReturnsReactorFactory() {
|
||||
ClientHttpRequestFactory requestFactory = ClientHttpRequestFactories.get(ReactorClientHttpRequestFactory.class,
|
||||
ClientHttpRequestFactorySettings.DEFAULTS);
|
||||
assertThat(requestFactory).isInstanceOf(ReactorClientHttpRequestFactory.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Deprecated(since = "3.2.0")
|
||||
@SuppressWarnings("removal")
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.webservices.client;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import io.netty.channel.ChannelOption;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
|
||||
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ReactorClientHttpRequestFactory;
|
||||
import org.springframework.ws.transport.WebServiceMessageSender;
|
||||
import org.springframework.ws.transport.http.ClientHttpRequestMessageSender;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpWebServiceMessageSenderBuilder} when Reactor Netty is the
|
||||
* predominant HTTP client.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar" })
|
||||
class HttpWebServiceMessageSenderBuilderReactorClientIntegrationTests {
|
||||
|
||||
private final HttpWebServiceMessageSenderBuilder builder = new HttpWebServiceMessageSenderBuilder();
|
||||
|
||||
@Test
|
||||
void buildUsesReactorClientIfHttpComponentsAndJettyAreNotAvailable() {
|
||||
WebServiceMessageSender messageSender = this.builder.build();
|
||||
assertReactorClientHttpRequestFactory(messageSender);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWithCustomTimeouts() {
|
||||
WebServiceMessageSender messageSender = this.builder.setConnectTimeout(Duration.ofSeconds(5))
|
||||
.setReadTimeout(Duration.ofSeconds(2))
|
||||
.build();
|
||||
ReactorClientHttpRequestFactory factory = assertReactorClientHttpRequestFactory(messageSender);
|
||||
assertThat(factory).extracting("httpClient", InstanceOfAssertFactories.type(HttpClient.class))
|
||||
.extracting((httpClient) -> httpClient.configuration().options(), InstanceOfAssertFactories.MAP)
|
||||
.containsEntry(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
|
||||
assertThat(factory).hasFieldOrPropertyWithValue("readTimeout", Duration.ofSeconds(2));
|
||||
}
|
||||
|
||||
private ReactorClientHttpRequestFactory assertReactorClientHttpRequestFactory(
|
||||
WebServiceMessageSender messageSender) {
|
||||
assertThat(messageSender).isInstanceOf(ClientHttpRequestMessageSender.class);
|
||||
ClientHttpRequestMessageSender sender = (ClientHttpRequestMessageSender) messageSender;
|
||||
ClientHttpRequestFactory requestFactory = sender.getRequestFactory();
|
||||
assertThat(requestFactory).isInstanceOf(ReactorClientHttpRequestFactory.class);
|
||||
return (ReactorClientHttpRequestFactory) requestFactory;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -34,7 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar", "okhttp*.jar" })
|
||||
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar", "okhttp*.jar", "reactor-netty-http-*.jar" })
|
||||
class HttpWebServiceMessageSenderBuilderSimpleIntegrationTests {
|
||||
|
||||
private final HttpWebServiceMessageSenderBuilder builder = new HttpWebServiceMessageSenderBuilder();
|
||||
|
||||
Reference in New Issue
Block a user