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 { *
    *
  1. {@link HttpComponentsClientHttpRequestFactory}
  2. *
  3. {@link JettyClientHttpRequestFactory}
  4. + *
  5. {@link ReactorClientHttpRequestFactory}
  6. *
  7. {@link org.springframework.http.client.OkHttp3ClientHttpRequestFactory * OkHttp3ClientHttpRequestFactory} (deprecated)
  8. *
  9. {@link SimpleClientHttpRequestFactory}
  10. @@ -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 { *
  11. {@link HttpComponentsClientHttpRequestFactory}
  12. *
  13. {@link JdkClientHttpRequestFactory}
  14. *
  15. {@link JettyClientHttpRequestFactory}
  16. + *
  17. {@link ReactorClientHttpRequestFactory}
  18. *
  19. {@link org.springframework.http.client.OkHttp3ClientHttpRequestFactory * OkHttp3ClientHttpRequestFactory} (deprecated)
  20. *
  21. {@link SimpleClientHttpRequestFactory}
  22. @@ -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();