Skip to content

Commit 68ed4b1

Browse files
committed
Add support for Reactor Netty to ClientHttpRequestFactories
Closes gh-42587
1 parent c9e548b commit 68ed4b1

File tree

9 files changed

+203
-5
lines changed

9 files changed

+203
-5
lines changed

spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ In order of preference, the following clients are supported:
194194

195195
. Apache HttpClient
196196
. Jetty HttpClient
197+
. Reactor Netty HttpClient
197198
. OkHttp (deprecated)
198199
. Simple JDK client (`HttpURLConnection`)
199200

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@
2727

2828
import javax.net.ssl.HttpsURLConnection;
2929
import javax.net.ssl.SSLContext;
30+
import javax.net.ssl.SSLException;
3031
import javax.net.ssl.SSLSocketFactory;
3132
import javax.net.ssl.TrustManager;
3233
import javax.net.ssl.X509TrustManager;
3334

35+
import io.netty.handler.ssl.SslContextBuilder;
3436
import okhttp3.OkHttpClient;
3537
import org.apache.hc.client5.http.classic.HttpClient;
3638
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
@@ -42,19 +44,23 @@
4244
import org.eclipse.jetty.client.transport.HttpClientTransportDynamic;
4345
import org.eclipse.jetty.io.ClientConnector;
4446
import org.eclipse.jetty.util.ssl.SslContextFactory;
47+
import reactor.netty.tcp.SslProvider.SslContextSpec;
4548

4649
import org.springframework.boot.context.properties.PropertyMapper;
4750
import org.springframework.boot.ssl.SslBundle;
51+
import org.springframework.boot.ssl.SslManagerBundle;
4852
import org.springframework.boot.ssl.SslOptions;
4953
import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper;
5054
import org.springframework.http.client.ClientHttpRequestFactory;
5155
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
5256
import org.springframework.http.client.JdkClientHttpRequestFactory;
5357
import org.springframework.http.client.JettyClientHttpRequestFactory;
58+
import org.springframework.http.client.ReactorClientHttpRequestFactory;
5459
import org.springframework.http.client.SimpleClientHttpRequestFactory;
5560
import org.springframework.util.Assert;
5661
import org.springframework.util.ClassUtils;
5762
import org.springframework.util.ReflectionUtils;
63+
import org.springframework.util.function.ThrowingConsumer;
5864

5965
/**
6066
* Utility class that can be used to create {@link ClientHttpRequestFactory} instances
@@ -79,6 +85,10 @@ public final class ClientHttpRequestFactories {
7985

8086
private static final boolean JETTY_CLIENT_PRESENT = ClassUtils.isPresent(JETTY_CLIENT_CLASS, null);
8187

88+
static final String REACTOR_CLIENT_CLASS = "reactor.netty.http.client.HttpClient";
89+
90+
private static final boolean REACTOR_CLIENT_PRESENT = ClassUtils.isPresent(REACTOR_CLIENT_CLASS, null);
91+
8292
private ClientHttpRequestFactories() {
8393
}
8494

@@ -89,6 +99,7 @@ private ClientHttpRequestFactories() {
8999
* <ol>
90100
* <li>{@link HttpComponentsClientHttpRequestFactory}</li>
91101
* <li>{@link JettyClientHttpRequestFactory}</li>
102+
* <li>{@link ReactorClientHttpRequestFactory}</li>
92103
* <li>{@link org.springframework.http.client.OkHttp3ClientHttpRequestFactory
93104
* OkHttp3ClientHttpRequestFactory} (deprecated)</li>
94105
* <li>{@link SimpleClientHttpRequestFactory}</li>
@@ -105,6 +116,9 @@ public static ClientHttpRequestFactory get(ClientHttpRequestFactorySettings sett
105116
if (JETTY_CLIENT_PRESENT) {
106117
return Jetty.get(settings);
107118
}
119+
if (REACTOR_CLIENT_PRESENT) {
120+
return Reactor.get(settings);
121+
}
108122
if (OKHTTP_CLIENT_PRESENT) {
109123
return OkHttp.get(settings);
110124
}
@@ -120,6 +134,7 @@ public static ClientHttpRequestFactory get(ClientHttpRequestFactorySettings sett
120134
* <li>{@link HttpComponentsClientHttpRequestFactory}</li>
121135
* <li>{@link JdkClientHttpRequestFactory}</li>
122136
* <li>{@link JettyClientHttpRequestFactory}</li>
137+
* <li>{@link ReactorClientHttpRequestFactory}</li>
123138
* <li>{@link org.springframework.http.client.OkHttp3ClientHttpRequestFactory
124139
* OkHttp3ClientHttpRequestFactory} (deprecated)</li>
125140
* <li>{@link SimpleClientHttpRequestFactory}</li>
@@ -144,6 +159,9 @@ public static <T extends ClientHttpRequestFactory> T get(Class<T> requestFactory
144159
if (requestFactoryType == JettyClientHttpRequestFactory.class) {
145160
return (T) Jetty.get(settings);
146161
}
162+
if (requestFactoryType == ReactorClientHttpRequestFactory.class) {
163+
return (T) Reactor.get(settings);
164+
}
147165
if (requestFactoryType == JdkClientHttpRequestFactory.class) {
148166
return (T) Jdk.get(settings);
149167
}
@@ -286,6 +304,41 @@ private static JettyClientHttpRequestFactory createRequestFactory(SslBundle sslB
286304

287305
}
288306

307+
/**
308+
* Support for {@link ReactorClientHttpRequestFactory}.
309+
*/
310+
static class Reactor {
311+
312+
static ReactorClientHttpRequestFactory get(ClientHttpRequestFactorySettings settings) {
313+
ReactorClientHttpRequestFactory requestFactory = createRequestFactory(settings.sslBundle());
314+
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
315+
map.from(settings::connectTimeout).asInt(Duration::toMillis).to(requestFactory::setConnectTimeout);
316+
map.from(settings::readTimeout).asInt(Duration::toMillis).to(requestFactory::setReadTimeout);
317+
return requestFactory;
318+
}
319+
320+
private static ReactorClientHttpRequestFactory createRequestFactory(SslBundle sslBundle) {
321+
if (sslBundle != null) {
322+
reactor.netty.http.client.HttpClient httpClient = reactor.netty.http.client.HttpClient.create()
323+
.secure((ThrowingConsumer.of((spec) -> configureSsl(spec, sslBundle))));
324+
return new ReactorClientHttpRequestFactory(httpClient);
325+
}
326+
return new ReactorClientHttpRequestFactory();
327+
}
328+
329+
private static void configureSsl(SslContextSpec spec, SslBundle sslBundle) throws SSLException {
330+
SslOptions options = sslBundle.getOptions();
331+
SslManagerBundle managers = sslBundle.getManagers();
332+
SslContextBuilder builder = SslContextBuilder.forClient()
333+
.keyManager(managers.getKeyManagerFactory())
334+
.trustManager(managers.getTrustManagerFactory())
335+
.ciphers(SslOptions.asSet(options.getCiphers()))
336+
.protocols(options.getEnabledProtocols());
337+
spec.sslContext(builder.build());
338+
}
339+
340+
}
341+
289342
/**
290343
* Support for {@link JdkClientHttpRequestFactory}.
291344
*/

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp3Tests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
* @deprecated since 3.2.0 for removal in 3.4.0
3737
*/
3838
@ClassPathOverrides("com.squareup.okhttp3:okhttp:3.14.9")
39-
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar" })
39+
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar", "reactor-netty-http-*.jar" })
4040
@Deprecated(since = "3.2.0", forRemoval = true)
4141
@SuppressWarnings("removal")
4242
class ClientHttpRequestFactoriesOkHttp3Tests

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp4Tests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
* @author Andy Wilkinson
3535
* @deprecated since 3.2.0 for removal in 3.4.0
3636
*/
37-
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar" })
37+
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar", "reactor-netty-http-*.jar" })
3838
@Deprecated(since = "3.2.0", forRemoval = true)
3939
@SuppressWarnings("removal")
4040
class ClientHttpRequestFactoriesOkHttp4Tests
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.web.client;
18+
19+
import java.time.Duration;
20+
21+
import io.netty.channel.ChannelOption;
22+
import reactor.netty.http.client.HttpClient;
23+
24+
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
25+
import org.springframework.http.client.ReactorClientHttpRequestFactory;
26+
import org.springframework.test.util.ReflectionTestUtils;
27+
28+
/**
29+
* Tests for {@link ClientHttpRequestFactories} when Reactor Netty is the predominant HTTP
30+
* client.
31+
*
32+
* @author Andy Wilkinson
33+
*/
34+
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar" })
35+
class ClientHttpRequestFactoriesReactorTests
36+
extends AbstractClientHttpRequestFactoriesTests<ReactorClientHttpRequestFactory> {
37+
38+
ClientHttpRequestFactoriesReactorTests() {
39+
super(ReactorClientHttpRequestFactory.class);
40+
}
41+
42+
@Override
43+
protected long connectTimeout(ReactorClientHttpRequestFactory requestFactory) {
44+
return (int) ((HttpClient) ReflectionTestUtils.getField(requestFactory, "httpClient")).configuration()
45+
.options()
46+
.get(ChannelOption.CONNECT_TIMEOUT_MILLIS);
47+
}
48+
49+
@Override
50+
protected long readTimeout(ReactorClientHttpRequestFactory requestFactory) {
51+
return ((Duration) ReflectionTestUtils.getField(requestFactory, "readTimeout")).toMillis();
52+
}
53+
54+
@Override
55+
protected boolean supportsSettingConnectTimeout() {
56+
return true;
57+
}
58+
59+
@Override
60+
protected boolean supportsSettingReadTimeout() {
61+
return true;
62+
}
63+
64+
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesSimpleTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
*
2727
* @author Andy Wilkinson
2828
*/
29-
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar", "okhttp-*.jar" })
29+
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar", "okhttp-*.jar", "reactor-netty-http-*.jar" })
3030
class ClientHttpRequestFactoriesSimpleTests
3131
extends AbstractClientHttpRequestFactoriesTests<SimpleClientHttpRequestFactory> {
3232

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesTests.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.http.client.ClientHttpRequestFactory;
2828
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
2929
import org.springframework.http.client.JdkClientHttpRequestFactory;
30+
import org.springframework.http.client.ReactorClientHttpRequestFactory;
3031
import org.springframework.http.client.SimpleClientHttpRequestFactory;
3132

3233
import static org.assertj.core.api.Assertions.assertThat;
@@ -67,6 +68,13 @@ void getOfHttpComponentsFactoryReturnsHttpComponentsFactory() {
6768
assertThat(requestFactory).isInstanceOf(HttpComponentsClientHttpRequestFactory.class);
6869
}
6970

71+
@Test
72+
void getOfReactorFactoryReturnsReactorFactory() {
73+
ClientHttpRequestFactory requestFactory = ClientHttpRequestFactories.get(ReactorClientHttpRequestFactory.class,
74+
ClientHttpRequestFactorySettings.DEFAULTS);
75+
assertThat(requestFactory).isInstanceOf(ReactorClientHttpRequestFactory.class);
76+
}
77+
7078
@Test
7179
@Deprecated(since = "3.2.0")
7280
@SuppressWarnings("removal")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.webservices.client;
18+
19+
import java.time.Duration;
20+
21+
import io.netty.channel.ChannelOption;
22+
import org.assertj.core.api.InstanceOfAssertFactories;
23+
import org.junit.jupiter.api.Test;
24+
import reactor.netty.http.client.HttpClient;
25+
26+
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
27+
import org.springframework.http.client.ClientHttpRequestFactory;
28+
import org.springframework.http.client.ReactorClientHttpRequestFactory;
29+
import org.springframework.ws.transport.WebServiceMessageSender;
30+
import org.springframework.ws.transport.http.ClientHttpRequestMessageSender;
31+
32+
import static org.assertj.core.api.Assertions.assertThat;
33+
34+
/**
35+
* Tests for {@link HttpWebServiceMessageSenderBuilder} when Reactor Netty is the
36+
* predominant HTTP client.
37+
*
38+
* @author Andy Wilkinson
39+
*/
40+
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar" })
41+
class HttpWebServiceMessageSenderBuilderReactorClientIntegrationTests {
42+
43+
private final HttpWebServiceMessageSenderBuilder builder = new HttpWebServiceMessageSenderBuilder();
44+
45+
@Test
46+
void buildUsesReactorClientIfHttpComponentsAndJettyAreNotAvailable() {
47+
WebServiceMessageSender messageSender = this.builder.build();
48+
assertReactorClientHttpRequestFactory(messageSender);
49+
}
50+
51+
@Test
52+
void buildWithCustomTimeouts() {
53+
WebServiceMessageSender messageSender = this.builder.setConnectTimeout(Duration.ofSeconds(5))
54+
.setReadTimeout(Duration.ofSeconds(2))
55+
.build();
56+
ReactorClientHttpRequestFactory factory = assertReactorClientHttpRequestFactory(messageSender);
57+
assertThat(factory).extracting("httpClient", InstanceOfAssertFactories.type(HttpClient.class))
58+
.extracting((httpClient) -> httpClient.configuration().options(), InstanceOfAssertFactories.MAP)
59+
.containsEntry(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
60+
assertThat(factory).hasFieldOrPropertyWithValue("readTimeout", Duration.ofSeconds(2));
61+
}
62+
63+
private ReactorClientHttpRequestFactory assertReactorClientHttpRequestFactory(
64+
WebServiceMessageSender messageSender) {
65+
assertThat(messageSender).isInstanceOf(ClientHttpRequestMessageSender.class);
66+
ClientHttpRequestMessageSender sender = (ClientHttpRequestMessageSender) messageSender;
67+
ClientHttpRequestFactory requestFactory = sender.getRequestFactory();
68+
assertThat(requestFactory).isInstanceOf(ReactorClientHttpRequestFactory.class);
69+
return (ReactorClientHttpRequestFactory) requestFactory;
70+
}
71+
72+
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderSimpleIntegrationTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -34,7 +34,7 @@
3434
*
3535
* @author Stephane Nicoll
3636
*/
37-
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar", "okhttp*.jar" })
37+
@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar", "okhttp*.jar", "reactor-netty-http-*.jar" })
3838
class HttpWebServiceMessageSenderBuilderSimpleIntegrationTests {
3939

4040
private final HttpWebServiceMessageSenderBuilder builder = new HttpWebServiceMessageSenderBuilder();

0 commit comments

Comments
 (0)