From 255ea92a574236205766ec267efe84bb2b1dcd7a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 18 Sep 2025 14:02:18 -0700 Subject: [PATCH] Add `HttpClientTransport` factory support Update `JettyClientHttpRequestFactoryBuilder` and `JettyClientHttpConnectorBuilder` with support for create the `HttpClientTransport` from a factory function. Closes gh-47251 --- .../JettyClientHttpRequestFactoryBuilder.java | 15 +++++++ .../http/client/JettyHttpClientBuilder.java | 40 +++++++++++++++---- .../JettyClientHttpConnectorBuilder.java | 15 +++++++ ...yClientHttpRequestFactoryBuilderTests.java | 21 ++++++++++ .../JettyClientHttpConnectorBuilderTests.java | 21 ++++++++++ 5 files changed, 105 insertions(+), 7 deletions(-) diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilder.java index 2f8b0be7157..dac0c73838d 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilder.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.Function; import java.util.function.UnaryOperator; import org.eclipse.jetty.client.HttpClient; @@ -78,6 +79,20 @@ public final class JettyClientHttpRequestFactoryBuilder this.httpClientBuilder.withCustomizer(httpClientCustomizer)); } + /** + * Return a new {@link JettyClientHttpRequestFactoryBuilder} that uses the given + * factory to create the {@link HttpClientTransport}. + * @param httpClientTransportFactory the {@link HttpClientTransport} factory to use + * @return a new {@link JettyClientHttpRequestFactoryBuilder} instance + * @since 4.0.0 + */ + public JettyClientHttpRequestFactoryBuilder withHttpClientTransportFactory( + Function httpClientTransportFactory) { + Assert.notNull(httpClientTransportFactory, "'httpClientTransportFactory' must not be null"); + return new JettyClientHttpRequestFactoryBuilder(getCustomizers(), + this.httpClientBuilder.withHttpClientTransportFactory(httpClientTransportFactory)); + } + /** * Return a new {@link JettyClientHttpRequestFactoryBuilder} that applies additional * customization to the underlying {@link HttpClientTransport}. diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyHttpClientBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyHttpClientBuilder.java index b0556d06ce1..bece30b2f6b 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyHttpClientBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/JettyHttpClientBuilder.java @@ -19,6 +19,7 @@ package org.springframework.boot.http.client; import java.time.Duration; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import java.util.function.Function; import javax.net.ssl.SSLContext; @@ -48,22 +49,31 @@ public final class JettyHttpClientBuilder { private final Consumer customizer; + private final Function httpClientTransportFactory; + private final Consumer httpClientTransportCustomizer; private final Consumer clientConnectorCustomizerCustomizer; public JettyHttpClientBuilder() { - this(Empty.consumer(), Empty.consumer(), Empty.consumer()); + this(Empty.consumer(), JettyHttpClientBuilder::createHttpClientTransport, Empty.consumer(), Empty.consumer()); } private JettyHttpClientBuilder(Consumer customizer, + Function httpClientTransportFactory, Consumer httpClientTransportCustomizer, Consumer clientConnectorCustomizerCustomizer) { this.customizer = customizer; + this.httpClientTransportFactory = httpClientTransportFactory; this.httpClientTransportCustomizer = httpClientTransportCustomizer; this.clientConnectorCustomizerCustomizer = clientConnectorCustomizerCustomizer; } + private static HttpClientTransport createHttpClientTransport(ClientConnector connector) { + return (connector.getSslContextFactory() != null) ? new HttpClientTransportDynamic(connector) + : new HttpClientTransportOverHTTP(connector); + } + /** * Return a new {@link JettyClientHttpRequestFactoryBuilder} that applies additional * customization to the underlying {@link HttpClient}. @@ -72,8 +82,22 @@ public final class JettyHttpClientBuilder { */ public JettyHttpClientBuilder withCustomizer(Consumer customizer) { Assert.notNull(customizer, "'customizer' must not be null"); - return new JettyHttpClientBuilder(this.customizer.andThen(customizer), this.httpClientTransportCustomizer, - this.clientConnectorCustomizerCustomizer); + return new JettyHttpClientBuilder(this.customizer.andThen(customizer), this.httpClientTransportFactory, + this.httpClientTransportCustomizer, this.clientConnectorCustomizerCustomizer); + } + + /** + * Return a new {@link JettyClientHttpRequestFactoryBuilder} that uses the given + * factory to create the {@link HttpClientTransport}. + * @param httpClientTransportFactory the {@link HttpClientTransport} factory to use + * @return a new {@link JettyClientHttpRequestFactoryBuilder} instance + * @since 4.0.0 + */ + public JettyHttpClientBuilder withHttpClientTransportFactory( + Function httpClientTransportFactory) { + Assert.notNull(httpClientTransportFactory, "'httpClientTransportFactory' must not be null"); + return new JettyHttpClientBuilder(this.customizer, httpClientTransportFactory, + this.httpClientTransportCustomizer, this.clientConnectorCustomizerCustomizer); } /** @@ -85,7 +109,7 @@ public final class JettyHttpClientBuilder { public JettyHttpClientBuilder withHttpClientTransportCustomizer( Consumer httpClientTransportCustomizer) { Assert.notNull(httpClientTransportCustomizer, "'httpClientTransportCustomizer' must not be null"); - return new JettyHttpClientBuilder(this.customizer, + return new JettyHttpClientBuilder(this.customizer, this.httpClientTransportFactory, this.httpClientTransportCustomizer.andThen(httpClientTransportCustomizer), this.clientConnectorCustomizerCustomizer); } @@ -99,7 +123,8 @@ public final class JettyHttpClientBuilder { public JettyHttpClientBuilder withClientConnectorCustomizerCustomizer( Consumer clientConnectorCustomizerCustomizer) { Assert.notNull(clientConnectorCustomizerCustomizer, "'clientConnectorCustomizerCustomizer' must not be null"); - return new JettyHttpClientBuilder(this.customizer, this.httpClientTransportCustomizer, + return new JettyHttpClientBuilder(this.customizer, this.httpClientTransportFactory, + this.httpClientTransportCustomizer, this.clientConnectorCustomizerCustomizer.andThen(clientConnectorCustomizerCustomizer)); } @@ -127,8 +152,9 @@ public final class JettyHttpClientBuilder { private HttpClientTransport createTransport(HttpClientSettings settings) { ClientConnector connector = createClientConnector(settings.sslBundle()); - return (connector.getSslContextFactory() != null) ? new HttpClientTransportDynamic(connector) - : new HttpClientTransportOverHTTP(connector); + HttpClientTransport clientTransport = this.httpClientTransportFactory.apply(connector); + Assert.state(clientTransport != null, "'httpClientTransportFactory' did not return a client transport"); + return clientTransport; } private ClientConnector createClientConnector(@Nullable SslBundle sslBundle) { diff --git a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/reactive/JettyClientHttpConnectorBuilder.java b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/reactive/JettyClientHttpConnectorBuilder.java index 69aac9ba2c1..c4fa95cacef 100644 --- a/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/reactive/JettyClientHttpConnectorBuilder.java +++ b/module/spring-boot-http-client/src/main/java/org/springframework/boot/http/client/reactive/JettyClientHttpConnectorBuilder.java @@ -19,6 +19,7 @@ package org.springframework.boot.http.client.reactive; import java.util.Collection; import java.util.List; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.UnaryOperator; import org.eclipse.jetty.client.HttpClient; @@ -62,6 +63,20 @@ public final class JettyClientHttpConnectorBuilder return new JettyClientHttpConnectorBuilder(mergedCustomizers(customizers), this.httpClientBuilder); } + /** + * Return a new {@link JettyClientHttpConnectorBuilder} that uses the given factory to + * create the {@link HttpClientTransport}. + * @param httpClientTransportFactory the {@link HttpClientTransport} factory to use + * @return a new {@link JettyClientHttpConnectorBuilder} instance + * @since 4.0.0 + */ + public JettyClientHttpConnectorBuilder withHttpClientTransportFactory( + Function httpClientTransportFactory) { + Assert.notNull(httpClientTransportFactory, "'httpClientTransportFactory' must not be null"); + return new JettyClientHttpConnectorBuilder(getCustomizers(), + this.httpClientBuilder.withHttpClientTransportFactory(httpClientTransportFactory)); + } + /** * Return a new {@link JettyClientHttpConnectorBuilder} that applies additional * customization to the underlying {@link HttpClient}. diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilderTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilderTests.java index af08d36513d..3c346c7763b 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilderTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilderTests.java @@ -18,12 +18,15 @@ package org.springframework.boot.http.client; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClientTransport; +import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; import org.eclipse.jetty.io.ClientConnector; import org.junit.jupiter.api.Test; import org.springframework.http.client.JettyClientHttpRequestFactory; import org.springframework.test.util.ReflectionTestUtils; +import static org.assertj.core.api.Assertions.assertThat; + /** * Tests for {@link JettyClientHttpRequestFactoryBuilder} and * {@link JettyHttpClientBuilder}. @@ -62,6 +65,16 @@ class JettyClientHttpRequestFactoryBuilderTests customizer.assertCalled(); } + @Test + void withHttpClientTransportFactory() { + JettyClientHttpRequestFactory factory = ClientHttpRequestFactoryBuilder.jetty() + .withHttpClientTransportFactory(TestHttpClientTransport::new) + .build(); + assertThat(factory).extracting("httpClient") + .extracting("transport") + .isInstanceOf(TestHttpClientTransport.class); + } + @Override protected long connectTimeout(JettyClientHttpRequestFactory requestFactory) { return ((HttpClient) ReflectionTestUtils.getField(requestFactory, "httpClient")).getConnectTimeout(); @@ -72,4 +85,12 @@ class JettyClientHttpRequestFactoryBuilderTests return (long) ReflectionTestUtils.getField(requestFactory, "readTimeout"); } + static class TestHttpClientTransport extends HttpClientTransportOverHTTP { + + TestHttpClientTransport(ClientConnector connector) { + super(connector); + } + + } + } diff --git a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/JettyClientHttpConnectorBuilderTests.java b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/JettyClientHttpConnectorBuilderTests.java index 45ab7804d08..660f14f9b2d 100644 --- a/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/JettyClientHttpConnectorBuilderTests.java +++ b/module/spring-boot-http-client/src/test/java/org/springframework/boot/http/client/reactive/JettyClientHttpConnectorBuilderTests.java @@ -20,6 +20,7 @@ import java.time.Duration; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClientTransport; +import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; import org.eclipse.jetty.io.ClientConnector; import org.junit.jupiter.api.Test; @@ -27,6 +28,8 @@ import org.springframework.boot.http.client.JettyHttpClientBuilder; import org.springframework.http.client.reactive.JettyClientHttpConnector; import org.springframework.test.util.ReflectionTestUtils; +import static org.assertj.core.api.Assertions.assertThat; + /** * Tests for {@link JettyClientHttpConnectorBuilder} and {@link JettyHttpClientBuilder}. * @@ -63,6 +66,16 @@ class JettyClientHttpConnectorBuilderTests extends AbstractClientHttpConnectorBu customizer.assertCalled(); } + @Test + void withHttpClientTransportFactory() { + JettyClientHttpConnector connector = ClientHttpConnectorBuilder.jetty() + .withHttpClientTransportFactory(TestHttpClientTransport::new) + .build(); + assertThat(connector).extracting("httpClient") + .extracting("transport") + .isInstanceOf(TestHttpClientTransport.class); + } + @Override protected long connectTimeout(JettyClientHttpConnector connector) { return ((HttpClient) ReflectionTestUtils.getField(connector, "httpClient")).getConnectTimeout(); @@ -74,4 +87,12 @@ class JettyClientHttpConnectorBuilderTests extends AbstractClientHttpConnectorBu return ((Duration) ReflectionTestUtils.getField(httpClient, "readTimeout")).toMillis(); } + static class TestHttpClientTransport extends HttpClientTransportOverHTTP { + + TestHttpClientTransport(ClientConnector connector) { + super(connector); + } + + } + }