From e1b59355078b09f3e3710a60b012bc39985926c8 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 25 Oct 2024 16:52:22 -0700 Subject: [PATCH] Allow customization of underlying ClientHttpRequestFactory components Update `ClientHttpRequestFactoryBuilder` implementations for `HttpComponents`, `Jdk`, `Jetty` and `Reactor` to allow customization of the underlying components. Closes gh-39035 --- ...stractClientHttpRequestFactoryBuilder.java | 8 ++ ...onentsClientHttpRequestFactoryBuilder.java | 123 +++++++++++++++--- .../JdkClientHttpRequestFactoryBuilder.java | 37 ++++-- .../JettyClientHttpRequestFactoryBuilder.java | 84 ++++++++++-- ...eactorClientHttpRequestFactoryBuilder.java | 28 +++- ...sClientHttpRequestFactoryBuilderTests.java | 44 +++++++ ...kClientHttpRequestFactoryBuilderTests.java | 14 ++ ...yClientHttpRequestFactoryBuilderTests.java | 21 +++ ...rClientHttpRequestFactoryBuilderTests.java | 24 ++++ .../boot/http/client/TestCustomizer.java | 42 ++++++ 10 files changed, 385 insertions(+), 40 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/TestCustomizer.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/AbstractClientHttpRequestFactoryBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/AbstractClientHttpRequestFactoryBuilder.java index a189a5eb66..5c82a10e95 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/AbstractClientHttpRequestFactoryBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/AbstractClientHttpRequestFactoryBuilder.java @@ -35,12 +35,20 @@ import org.springframework.util.Assert; abstract class AbstractClientHttpRequestFactoryBuilder implements ClientHttpRequestFactoryBuilder { + private static final Consumer EMPTY_CUSTOMIZER = (t) -> { + }; + private final List> customizers; protected AbstractClientHttpRequestFactoryBuilder(List> customizers) { this.customizers = (customizers != null) ? customizers : Collections.emptyList(); } + @SuppressWarnings("unchecked") + protected static Consumer emptyCustomizer() { + return (Consumer) EMPTY_CUSTOMIZER; + } + protected final List> getCustomizers() { return this.customizers; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilder.java index 2e12294567..5f8436af86 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilder.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import java.util.function.Function; import org.apache.hc.client5.http.classic.HttpClient; import org.apache.hc.client5.http.impl.DefaultRedirectStrategy; @@ -32,6 +33,7 @@ import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuil import org.apache.hc.client5.http.protocol.RedirectStrategy; import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier; +import org.apache.hc.client5.http.ssl.TlsSocketStrategy; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.io.SocketConfig; @@ -42,6 +44,7 @@ import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Red import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslOptions; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** @@ -55,25 +58,104 @@ import org.springframework.util.ClassUtils; public final class HttpComponentsClientHttpRequestFactoryBuilder extends AbstractClientHttpRequestFactoryBuilder { + private final Consumer httpClientCustomizer; + + private final Consumer connectionManagerCustomizer; + + private final Consumer socketConfigCustomizer; + + private final Function tlsSocketStrategyFactory; + HttpComponentsClientHttpRequestFactoryBuilder() { - this(Collections.emptyList()); + this(Collections.emptyList(), emptyCustomizer(), emptyCustomizer(), emptyCustomizer(), + HttpComponentsClientHttpRequestFactoryBuilder::createTlsSocketStrategy); } private HttpComponentsClientHttpRequestFactoryBuilder( - List> customizers) { + List> customizers, + Consumer httpClientCustomizer, + Consumer connectionManagerCustomizer, + Consumer socketConfigCustomizer, + Function tlsSocketStrategyFactory) { super(customizers); + this.httpClientCustomizer = httpClientCustomizer; + this.connectionManagerCustomizer = connectionManagerCustomizer; + this.socketConfigCustomizer = socketConfigCustomizer; + this.tlsSocketStrategyFactory = tlsSocketStrategyFactory; } @Override public HttpComponentsClientHttpRequestFactoryBuilder withCustomizer( Consumer customizer) { - return new HttpComponentsClientHttpRequestFactoryBuilder(mergedCustomizers(customizer)); + return new HttpComponentsClientHttpRequestFactoryBuilder(mergedCustomizers(customizer), + this.httpClientCustomizer, this.connectionManagerCustomizer, this.socketConfigCustomizer, + this.tlsSocketStrategyFactory); } @Override public HttpComponentsClientHttpRequestFactoryBuilder withCustomizers( Collection> customizers) { - return new HttpComponentsClientHttpRequestFactoryBuilder(mergedCustomizers(customizers)); + return new HttpComponentsClientHttpRequestFactoryBuilder(mergedCustomizers(customizers), + this.httpClientCustomizer, this.connectionManagerCustomizer, this.socketConfigCustomizer, + this.tlsSocketStrategyFactory); + } + + /** + * Return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} that applies + * additional customization to the underlying {@link HttpClientBuilder}. + * @param httpClientCustomizer the customizer to apply + * @return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} instance + */ + public HttpComponentsClientHttpRequestFactoryBuilder withHttpClientCustomizer( + Consumer httpClientCustomizer) { + Assert.notNull(httpClientCustomizer, "'httpClientCustomizer' must not be null"); + return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(), + this.httpClientCustomizer.andThen(httpClientCustomizer), this.connectionManagerCustomizer, + this.socketConfigCustomizer, this.tlsSocketStrategyFactory); + } + + /** + * Return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} that applies + * additional customization to the underlying + * {@link PoolingHttpClientConnectionManagerBuilder}. + * @param connectionManagerCustomizer the customizer to apply + * @return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} instance + */ + public HttpComponentsClientHttpRequestFactoryBuilder withConnectionManagerCustomizer( + Consumer connectionManagerCustomizer) { + Assert.notNull(connectionManagerCustomizer, "'connectionManagerCustomizer' must not be null"); + return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(), this.httpClientCustomizer, + this.connectionManagerCustomizer.andThen(connectionManagerCustomizer), this.socketConfigCustomizer, + this.tlsSocketStrategyFactory); + } + + /** + * Return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} that applies + * additional customization to the underlying + * {@link org.apache.hc.core5.http.io.SocketConfig.Builder}. + * @param socketConfigCustomizer the customizer to apply + * @return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} instance + */ + public HttpComponentsClientHttpRequestFactoryBuilder withSocketConfigCustomizer( + Consumer socketConfigCustomizer) { + Assert.notNull(socketConfigCustomizer, "'socketConfigCustomizer' must not be null"); + return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(), this.httpClientCustomizer, + this.connectionManagerCustomizer, this.socketConfigCustomizer.andThen(socketConfigCustomizer), + this.tlsSocketStrategyFactory); + } + + /** + * Return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} with a + * replacement {@link TlsSocketStrategy} factory. + * @param tlsSocketStrategyFactory the new factory used to create a + * {@link TlsSocketStrategy} for a given {@link SslBundle} + * @return a new {@link HttpComponentsClientHttpRequestFactoryBuilder} instance + */ + public HttpComponentsClientHttpRequestFactoryBuilder withTlsSocketStrategyFactory( + Function tlsSocketStrategyFactory) { + Assert.notNull(tlsSocketStrategyFactory, "'tlsSocketStrategyFactory' must not be null"); + return new HttpComponentsClientHttpRequestFactoryBuilder(getCustomizers(), this.httpClientCustomizer, + this.connectionManagerCustomizer, this.socketConfigCustomizer, tlsSocketStrategyFactory); } @Override @@ -87,11 +169,12 @@ public final class HttpComponentsClientHttpRequestFactoryBuilder } private HttpClient createHttpClient(ClientHttpRequestFactorySettings settings) { - return HttpClientBuilder.create() + HttpClientBuilder builder = HttpClientBuilder.create() .useSystemProperties() .setRedirectStrategy(asRedirectStrategy(settings.redirects())) - .setConnectionManager(createConnectionManager(settings)) - .build(); + .setConnectionManager(createConnectionManager(settings)); + this.httpClientCustomizer.accept(builder); + return builder.build(); } private RedirectStrategy asRedirectStrategy(Redirects redirects) { @@ -102,23 +185,31 @@ public final class HttpComponentsClientHttpRequestFactoryBuilder } private PoolingHttpClientConnectionManager createConnectionManager(ClientHttpRequestFactorySettings settings) { - PoolingHttpClientConnectionManagerBuilder builder = PoolingHttpClientConnectionManagerBuilder.create(); + PoolingHttpClientConnectionManagerBuilder builder = PoolingHttpClientConnectionManagerBuilder.create() + .useSystemProperties(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(settings::readTimeout).as(this::createSocketConfig).to(builder::setDefaultSocketConfig); - map.from(settings::sslBundle).as(this::createTlsSocketStrategy).to(builder::setTlsSocketStrategy); - return builder.useSystemProperties().build(); + builder.setDefaultSocketConfig(createSocketConfig(settings)); + map.from(settings::sslBundle).as(this.tlsSocketStrategyFactory).to(builder::setTlsSocketStrategy); + this.connectionManagerCustomizer.accept(builder); + return builder.build(); } - private DefaultClientTlsStrategy createTlsSocketStrategy(SslBundle sslBundle) { + private SocketConfig createSocketConfig(ClientHttpRequestFactorySettings settings) { + SocketConfig.Builder builder = SocketConfig.custom(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(settings::readTimeout) + .asInt(Duration::toMillis) + .to((timeout) -> builder.setSoTimeout(timeout, TimeUnit.MILLISECONDS)); + this.socketConfigCustomizer.accept(builder); + return builder.build(); + } + + private static TlsSocketStrategy createTlsSocketStrategy(SslBundle sslBundle) { SslOptions options = sslBundle.getOptions(); return new DefaultClientTlsStrategy(sslBundle.createSslContext(), options.getEnabledProtocols(), options.getCiphers(), null, new DefaultHostnameVerifier()); } - private SocketConfig createSocketConfig(Duration readTimeout) { - return SocketConfig.custom().setSoTimeout((int) readTimeout.toMillis(), TimeUnit.MILLISECONDS).build(); - } - /** * {@link RedirectStrategy} that never follows redirects. */ diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilder.java index 9523e3cc93..a67294e7a7 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilder.java @@ -26,6 +26,7 @@ import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects; import org.springframework.boot.ssl.SslBundle; import org.springframework.http.client.JdkClientHttpRequestFactory; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** @@ -39,23 +40,40 @@ import org.springframework.util.ClassUtils; public class JdkClientHttpRequestFactoryBuilder extends AbstractClientHttpRequestFactoryBuilder { + private final Consumer httpClientCustomizer; + JdkClientHttpRequestFactoryBuilder() { - this(null); + this(null, emptyCustomizer()); } - private JdkClientHttpRequestFactoryBuilder(List> customizers) { + private JdkClientHttpRequestFactoryBuilder(List> customizers, + Consumer httpClientCustomizer) { super(customizers); + this.httpClientCustomizer = httpClientCustomizer; } @Override public JdkClientHttpRequestFactoryBuilder withCustomizer(Consumer customizer) { - return new JdkClientHttpRequestFactoryBuilder(mergedCustomizers(customizer)); + return new JdkClientHttpRequestFactoryBuilder(mergedCustomizers(customizer), this.httpClientCustomizer); } @Override public JdkClientHttpRequestFactoryBuilder withCustomizers( Collection> customizers) { - return new JdkClientHttpRequestFactoryBuilder(mergedCustomizers(customizers)); + return new JdkClientHttpRequestFactoryBuilder(mergedCustomizers(customizers), this.httpClientCustomizer); + } + + /** + * Return a new {@link JdkClientHttpRequestFactoryBuilder} that applies additional + * customization to the underlying {@link java.net.http.HttpClient.Builder}. + * @param httpClientCustomizer the customizer to apply + * @return a new {@link JdkClientHttpRequestFactoryBuilder} instance + */ + public JdkClientHttpRequestFactoryBuilder withHttpClientCustomizer( + Consumer httpClientCustomizer) { + Assert.notNull(httpClientCustomizer, "'httpClientCustomizer' must not be null"); + return new JdkClientHttpRequestFactoryBuilder(getCustomizers(), + this.httpClientCustomizer.andThen(httpClientCustomizer)); } @Override @@ -68,12 +86,13 @@ public class JdkClientHttpRequestFactoryBuilder } private HttpClient createHttpClient(ClientHttpRequestFactorySettings settings) { - HttpClient.Builder httpClientBuilder = HttpClient.newBuilder(); + HttpClient.Builder builder = HttpClient.newBuilder(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(settings::connectTimeout).to(httpClientBuilder::connectTimeout); - map.from(settings::sslBundle).as(SslBundle::createSslContext).to(httpClientBuilder::sslContext); - map.from(settings::redirects).as(this::asHttpClientRedirect).to(httpClientBuilder::followRedirects); - return httpClientBuilder.build(); + map.from(settings::connectTimeout).to(builder::connectTimeout); + map.from(settings::sslBundle).as(SslBundle::createSslContext).to(builder::sslContext); + map.from(settings::redirects).as(this::asHttpClientRedirect).to(builder::followRedirects); + this.httpClientCustomizer.accept(builder); + return builder.build(); } private Redirect asHttpClientRedirect(Redirects redirects) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilder.java index d7a650fdba..bcc44fe968 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilder.java @@ -34,6 +34,7 @@ import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects; import org.springframework.boot.ssl.SslBundle; import org.springframework.http.client.JettyClientHttpRequestFactory; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** @@ -47,23 +48,77 @@ import org.springframework.util.ClassUtils; public final class JettyClientHttpRequestFactoryBuilder extends AbstractClientHttpRequestFactoryBuilder { + private final Consumer httpClientCustomizer; + + private final Consumer httpClientTransportCustomizer; + + private final Consumer clientConnectorCustomizerCustomizer; + JettyClientHttpRequestFactoryBuilder() { - this(null); + this(null, emptyCustomizer(), emptyCustomizer(), emptyCustomizer()); } - private JettyClientHttpRequestFactoryBuilder(List> customizers) { + private JettyClientHttpRequestFactoryBuilder(List> customizers, + Consumer httpClientCustomizer, Consumer httpClientTransportCustomizer, + Consumer clientConnectorCustomizerCustomizer) { super(customizers); + this.httpClientCustomizer = httpClientCustomizer; + this.httpClientTransportCustomizer = httpClientTransportCustomizer; + this.clientConnectorCustomizerCustomizer = clientConnectorCustomizerCustomizer; } @Override public JettyClientHttpRequestFactoryBuilder withCustomizer(Consumer customizer) { - return new JettyClientHttpRequestFactoryBuilder(mergedCustomizers(customizer)); + return new JettyClientHttpRequestFactoryBuilder(mergedCustomizers(customizer), this.httpClientCustomizer, + this.httpClientTransportCustomizer, this.clientConnectorCustomizerCustomizer); } @Override public JettyClientHttpRequestFactoryBuilder withCustomizers( Collection> customizers) { - return new JettyClientHttpRequestFactoryBuilder(mergedCustomizers(customizers)); + return new JettyClientHttpRequestFactoryBuilder(mergedCustomizers(customizers), this.httpClientCustomizer, + this.httpClientTransportCustomizer, this.clientConnectorCustomizerCustomizer); + } + + /** + * Return a new {@link JettyClientHttpRequestFactoryBuilder} that applies additional + * customization to the underlying {@link HttpClient}. + * @param httpClientCustomizer the customizer to apply + * @return a new {@link JettyClientHttpRequestFactoryBuilder} instance + */ + public JettyClientHttpRequestFactoryBuilder withHttpClientCustomizer(Consumer httpClientCustomizer) { + Assert.notNull(httpClientCustomizer, "'httpClientCustomizer' must not be null"); + return new JettyClientHttpRequestFactoryBuilder(getCustomizers(), + this.httpClientCustomizer.andThen(httpClientCustomizer), this.httpClientTransportCustomizer, + this.clientConnectorCustomizerCustomizer); + } + + /** + * Return a new {@link JettyClientHttpRequestFactoryBuilder} that applies additional + * customization to the underlying {@link HttpClientTransport}. + * @param httpClientTransportCustomizer the customizer to apply + * @return a new {@link JettyClientHttpRequestFactoryBuilder} instance + */ + public JettyClientHttpRequestFactoryBuilder withHttpClientTransportCustomizer( + Consumer httpClientTransportCustomizer) { + Assert.notNull(httpClientTransportCustomizer, "'httpClientTransportCustomizer' must not be null"); + return new JettyClientHttpRequestFactoryBuilder(getCustomizers(), this.httpClientCustomizer, + this.httpClientTransportCustomizer.andThen(httpClientTransportCustomizer), + this.clientConnectorCustomizerCustomizer); + } + + /** + * Return a new {@link JettyClientHttpRequestFactoryBuilder} that applies additional + * customization to the underlying {@link ClientConnector}. + * @param clientConnectorCustomizerCustomizer the customizer to apply + * @return a new {@link JettyClientHttpRequestFactoryBuilder} instance + */ + public JettyClientHttpRequestFactoryBuilder withClientConnectorCustomizerCustomizer( + Consumer clientConnectorCustomizerCustomizer) { + Assert.notNull(clientConnectorCustomizerCustomizer, "'clientConnectorCustomizerCustomizer' must not be null"); + return new JettyClientHttpRequestFactoryBuilder(getCustomizers(), this.httpClientCustomizer, + this.httpClientTransportCustomizer, + this.clientConnectorCustomizerCustomizer.andThen(clientConnectorCustomizerCustomizer)); } @Override @@ -77,27 +132,34 @@ public final class JettyClientHttpRequestFactoryBuilder private JettyClientHttpRequestFactory createRequestFactory(ClientHttpRequestFactorySettings settings) { HttpClientTransport transport = createTransport(settings); + this.httpClientTransportCustomizer.accept(transport); HttpClient httpClient = new HttpClient(transport); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(settings::redirects).as(this::followRedirects).to(httpClient::setFollowRedirects); + this.httpClientCustomizer.accept(httpClient); return new JettyClientHttpRequestFactory(httpClient); } private HttpClientTransport createTransport(ClientHttpRequestFactorySettings settings) { - if (settings.sslBundle() == null) { - return new HttpClientTransportOverHTTP(); - } ClientConnector connector = createClientConnector(settings.sslBundle()); - return new HttpClientTransportDynamic(connector); + return (connector.getSslContextFactory() != null) ? new HttpClientTransportDynamic(connector) + : new HttpClientTransportOverHTTP(connector); } private ClientConnector createClientConnector(SslBundle sslBundle) { + ClientConnector connector = new ClientConnector(); + if (sslBundle != null) { + connector.setSslContextFactory(createSslContextFactory(sslBundle)); + } + this.clientConnectorCustomizerCustomizer.accept(connector); + return connector; + } + + private SslContextFactory.Client createSslContextFactory(SslBundle sslBundle) { SSLContext sslContext = sslBundle.createSslContext(); SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); sslContextFactory.setSslContext(sslContext); - ClientConnector connector = new ClientConnector(); - connector.setSslContextFactory(sslContextFactory); - return connector; + return sslContextFactory; } private boolean followRedirects(Redirects redirects) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilder.java index 4a42d6995b..e3206cc0af 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilder.java @@ -20,6 +20,7 @@ import java.time.Duration; import java.util.Collection; import java.util.List; import java.util.function.Consumer; +import java.util.function.UnaryOperator; import javax.net.ssl.SSLException; @@ -33,6 +34,7 @@ import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslManagerBundle; import org.springframework.boot.ssl.SslOptions; import org.springframework.http.client.ReactorClientHttpRequestFactory; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.function.ThrowingConsumer; @@ -47,23 +49,40 @@ import org.springframework.util.function.ThrowingConsumer; public final class ReactorClientHttpRequestFactoryBuilder extends AbstractClientHttpRequestFactoryBuilder { + private final UnaryOperator httpClientCustomizer; + ReactorClientHttpRequestFactoryBuilder() { - this(null); + this(null, UnaryOperator.identity()); } - private ReactorClientHttpRequestFactoryBuilder(List> customizers) { + private ReactorClientHttpRequestFactoryBuilder(List> customizers, + UnaryOperator httpClientCustomizer) { super(customizers); + this.httpClientCustomizer = httpClientCustomizer; } @Override public ReactorClientHttpRequestFactoryBuilder withCustomizer(Consumer customizer) { - return new ReactorClientHttpRequestFactoryBuilder(mergedCustomizers(customizer)); + return new ReactorClientHttpRequestFactoryBuilder(mergedCustomizers(customizer), this.httpClientCustomizer); } @Override public ReactorClientHttpRequestFactoryBuilder withCustomizers( Collection> customizers) { - return new ReactorClientHttpRequestFactoryBuilder(mergedCustomizers(customizers)); + return new ReactorClientHttpRequestFactoryBuilder(mergedCustomizers(customizers), this.httpClientCustomizer); + } + + /** + * Return a new {@link ReactorClientHttpRequestFactoryBuilder} that applies additional + * customization to the underlying {@link HttpClient}. + * @param httpClientCustomizer the customizer to apply + * @return a new {@link ReactorClientHttpRequestFactoryBuilder} instance + */ + public ReactorClientHttpRequestFactoryBuilder withHttpClientCustomizer( + UnaryOperator httpClientCustomizer) { + Assert.notNull(httpClientCustomizer, "'httpClientCustomizer' must not be null"); + return new ReactorClientHttpRequestFactoryBuilder(getCustomizers(), + (t) -> httpClientCustomizer.apply(this.httpClientCustomizer.apply(t))); } @Override @@ -82,6 +101,7 @@ public final class ReactorClientHttpRequestFactoryBuilder if (settings.sslBundle() != null) { httpClient = httpClient.secure((ThrowingConsumer.of((spec) -> configureSsl(spec, settings.sslBundle())))); } + httpClient = this.httpClientCustomizer.apply(httpClient); return new ReactorClientHttpRequestFactory(httpClient); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilderTests.java index 36f60c9e42..240da097b8 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/HttpComponentsClientHttpRequestFactoryBuilderTests.java @@ -16,14 +16,26 @@ package org.springframework.boot.http.client; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + import org.apache.hc.client5.http.HttpRoute; import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.ssl.TlsSocketStrategy; import org.apache.hc.core5.function.Resolver; import org.apache.hc.core5.http.io.SocketConfig; +import org.apache.hc.core5.http.io.SocketConfig.Builder; +import org.junit.jupiter.api.Test; +import org.springframework.boot.ssl.SslBundle; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.test.util.ReflectionTestUtils; +import static org.assertj.core.api.Assertions.assertThat; + /** * Tests for {@link HttpComponentsClientHttpRequestFactoryBuilder}. * @@ -37,6 +49,38 @@ class HttpComponentsClientHttpRequestFactoryBuilderTests super(HttpComponentsClientHttpRequestFactory.class, ClientHttpRequestFactoryBuilder.httpComponents()); } + @Test + void withCustomizers() { + TestCustomizer httpClientCustomizer1 = new TestCustomizer<>(); + TestCustomizer httpClientCustomizer2 = new TestCustomizer<>(); + TestCustomizer connectionManagerCustomizer = new TestCustomizer<>(); + TestCustomizer socketConfigCustomizer = new TestCustomizer<>(); + ClientHttpRequestFactoryBuilder.httpComponents() + .withHttpClientCustomizer(httpClientCustomizer1) + .withHttpClientCustomizer(httpClientCustomizer2) + .withConnectionManagerCustomizer(connectionManagerCustomizer) + .withSocketConfigCustomizer(socketConfigCustomizer) + .build(); + httpClientCustomizer1.assertCalled(); + httpClientCustomizer2.assertCalled(); + connectionManagerCustomizer.assertCalled(); + socketConfigCustomizer.assertCalled(); + } + + @Test + void withTlsSocketStrategyFactory() { + ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.ofSslBundle(sslBundle()); + List bundles = new ArrayList<>(); + Function tlsSocketStrategyFactory = (bundle) -> { + bundles.add(bundle); + return (socket, target, port, attachment, context) -> null; + }; + ClientHttpRequestFactoryBuilder.httpComponents() + .withTlsSocketStrategyFactory(tlsSocketStrategyFactory) + .build(settings); + assertThat(bundles).contains(settings.sslBundle()); + } + @Override protected long connectTimeout(HttpComponentsClientHttpRequestFactory requestFactory) { return (long) ReflectionTestUtils.getField(requestFactory, "connectTimeout"); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilderTests.java index edc8b2258d..4187708d61 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilderTests.java @@ -19,6 +19,8 @@ package org.springframework.boot.http.client; import java.net.http.HttpClient; import java.time.Duration; +import org.junit.jupiter.api.Test; + import org.springframework.http.client.JdkClientHttpRequestFactory; import org.springframework.test.util.ReflectionTestUtils; @@ -34,6 +36,18 @@ class JdkClientHttpRequestFactoryBuilderTests super(JdkClientHttpRequestFactory.class, ClientHttpRequestFactoryBuilder.jdk()); } + @Test + void withCustomizers() { + TestCustomizer httpClientCustomizer1 = new TestCustomizer<>(); + TestCustomizer httpClientCustomizer2 = new TestCustomizer<>(); + ClientHttpRequestFactoryBuilder.jdk() + .withHttpClientCustomizer(httpClientCustomizer1) + .withHttpClientCustomizer(httpClientCustomizer2) + .build(); + httpClientCustomizer1.assertCalled(); + httpClientCustomizer2.assertCalled(); + } + @Override protected long connectTimeout(JdkClientHttpRequestFactory requestFactory) { HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(requestFactory, "httpClient"); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilderTests.java index 627eaf2776..d3a0b883b4 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilderTests.java @@ -17,6 +17,9 @@ package org.springframework.boot.http.client; import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpClientTransport; +import org.eclipse.jetty.io.ClientConnector; +import org.junit.jupiter.api.Test; import org.springframework.http.client.JettyClientHttpRequestFactory; import org.springframework.test.util.ReflectionTestUtils; @@ -33,6 +36,24 @@ class JettyClientHttpRequestFactoryBuilderTests super(JettyClientHttpRequestFactory.class, ClientHttpRequestFactoryBuilder.jetty()); } + @Test + void withCustomizers() { + TestCustomizer httpClientCustomizer1 = new TestCustomizer<>(); + TestCustomizer httpClientCustomizer2 = new TestCustomizer<>(); + TestCustomizer httpClientTransportCustomizer = new TestCustomizer<>(); + TestCustomizer clientConnectorCustomizerCustomizer = new TestCustomizer<>(); + ClientHttpRequestFactoryBuilder.jetty() + .withHttpClientCustomizer(httpClientCustomizer1) + .withHttpClientCustomizer(httpClientCustomizer2) + .withHttpClientTransportCustomizer(httpClientTransportCustomizer) + .withClientConnectorCustomizerCustomizer(clientConnectorCustomizerCustomizer) + .build(); + httpClientCustomizer1.assertCalled(); + httpClientCustomizer2.assertCalled(); + httpClientTransportCustomizer.assertCalled(); + clientConnectorCustomizerCustomizer.assertCalled(); + } + @Override protected long connectTimeout(JettyClientHttpRequestFactory requestFactory) { return ((HttpClient) ReflectionTestUtils.getField(requestFactory, "httpClient")).getConnectTimeout(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilderTests.java index 81e624e608..3025e698f4 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ReactorClientHttpRequestFactoryBuilderTests.java @@ -17,13 +17,19 @@ package org.springframework.boot.http.client; import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.function.UnaryOperator; import io.netty.channel.ChannelOption; +import org.junit.jupiter.api.Test; import reactor.netty.http.client.HttpClient; import org.springframework.http.client.ReactorClientHttpRequestFactory; import org.springframework.test.util.ReflectionTestUtils; +import static org.assertj.core.api.Assertions.assertThat; + /** * Tests for {@link ReactorClientHttpRequestFactoryBuilder}. * @@ -37,6 +43,24 @@ class ReactorClientHttpRequestFactoryBuilderTests super(ReactorClientHttpRequestFactory.class, ClientHttpRequestFactoryBuilder.reactor()); } + @Test + void withCustomizers() { + List httpClients = new ArrayList<>(); + UnaryOperator httpClientCustomizer1 = (httpClient) -> { + httpClients.add(httpClient); + return httpClient; + }; + UnaryOperator httpClientCustomizer2 = (httpClient) -> { + httpClients.add(httpClient); + return httpClient; + }; + ClientHttpRequestFactoryBuilder.reactor() + .withHttpClientCustomizer(httpClientCustomizer1) + .withHttpClientCustomizer(httpClientCustomizer2) + .build(); + assertThat(httpClients).hasSize(2); + } + @Override protected long connectTimeout(ReactorClientHttpRequestFactory requestFactory) { return (int) ((HttpClient) ReflectionTestUtils.getField(requestFactory, "httpClient")).configuration() diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/TestCustomizer.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/TestCustomizer.java new file mode 100644 index 0000000000..1d59ecb905 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/TestCustomizer.java @@ -0,0 +1,42 @@ +/* + * 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.http.client; + +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test customizer that can assert that it has been called. + * + * @param type being customized + * @author Phillip Webb + */ +class TestCustomizer implements Consumer { + + private boolean called; + + @Override + public void accept(T t) { + this.called = true; + } + + void assertCalled() { + assertThat(this.called).isTrue(); + } + +}