Add support for Reactor Netty to ClientHttpRequestFactories

Closes gh-42587
This commit is contained in:
Andy Wilkinson
2024-10-18 10:11:02 +01:00
parent c9e548b23b
commit 68ed4b1d4f
9 changed files with 203 additions and 5 deletions

View File

@@ -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`)

View File

@@ -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}.
*/

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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> {

View File

@@ -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")

View File

@@ -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;
}
}

View File

@@ -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();