diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc
index 8ba89d9df0..ebea6544a9 100644
--- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc
+++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/io/rest-client.adoc
@@ -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`)
diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java
index 8a1efdcecd..871c886c7a 100644
--- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java
+++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java
@@ -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 {
*
* - {@link HttpComponentsClientHttpRequestFactory}
* - {@link JettyClientHttpRequestFactory}
+ * - {@link ReactorClientHttpRequestFactory}
* - {@link org.springframework.http.client.OkHttp3ClientHttpRequestFactory
* OkHttp3ClientHttpRequestFactory} (deprecated)
* - {@link SimpleClientHttpRequestFactory}
@@ -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 {
* - {@link HttpComponentsClientHttpRequestFactory}
* - {@link JdkClientHttpRequestFactory}
* - {@link JettyClientHttpRequestFactory}
+ * - {@link ReactorClientHttpRequestFactory}
* - {@link org.springframework.http.client.OkHttp3ClientHttpRequestFactory
* OkHttp3ClientHttpRequestFactory} (deprecated)
* - {@link SimpleClientHttpRequestFactory}
@@ -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}.
*/
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp3Tests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp3Tests.java
index 1892282885..b6ab608200 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp3Tests.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp3Tests.java
@@ -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
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp4Tests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp4Tests.java
index 6e03f45918..4bf6be2976 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp4Tests.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp4Tests.java
@@ -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
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesReactorTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesReactorTests.java
new file mode 100644
index 0000000000..c538014ddf
--- /dev/null
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesReactorTests.java
@@ -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 {
+
+ 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;
+ }
+
+}
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesSimpleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesSimpleTests.java
index 9cb1387aa8..4b40bd988c 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesSimpleTests.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesSimpleTests.java
@@ -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 {
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesTests.java
index f9c0e85b9b..577aacc07e 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesTests.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesTests.java
@@ -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")
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderReactorClientIntegrationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderReactorClientIntegrationTests.java
new file mode 100644
index 0000000000..b315d8d23e
--- /dev/null
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderReactorClientIntegrationTests.java
@@ -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;
+ }
+
+}
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderSimpleIntegrationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderSimpleIntegrationTests.java
index 6c3a0b2ef1..07d1e22c08 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderSimpleIntegrationTests.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderSimpleIntegrationTests.java
@@ -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();