Add HttpClient factory support to ReactorHttpClientBuilder

Improve `ReactorHttpClientBuilder` support for creating the initial
`HttpClient`.

Fixes gh-45378
This commit is contained in:
Phillip Webb
2025-05-06 21:56:26 -07:00
parent 62c8d6bcb2
commit 55ba0985d3
5 changed files with 132 additions and 4 deletions

View File

@@ -20,12 +20,14 @@ import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import reactor.netty.http.client.HttpClient;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.http.client.ReactorClientHttpRequestFactory;
import org.springframework.http.client.ReactorResourceFactory;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -63,6 +65,33 @@ public final class ReactorClientHttpRequestFactoryBuilder
return new ReactorClientHttpRequestFactoryBuilder(mergedCustomizers(customizers), this.httpClientBuilder);
}
/**
* Return a new {@link ReactorClientHttpRequestFactoryBuilder} that uses the given
* {@link ReactorResourceFactory} to create the underlying {@link HttpClient}.
* @param reactorResourceFactory the {@link ReactorResourceFactory} to use
* @return a new {@link ReactorClientHttpRequestFactoryBuilder} instance
* @since 3.5.0
*/
public ReactorClientHttpRequestFactoryBuilder withReactorResourceFactory(
ReactorResourceFactory reactorResourceFactory) {
Assert.notNull(reactorResourceFactory, "'reactorResourceFactory' must not be null");
return new ReactorClientHttpRequestFactoryBuilder(getCustomizers(),
this.httpClientBuilder.withReactorResourceFactory(reactorResourceFactory));
}
/**
* Return a new {@link ReactorClientHttpRequestFactoryBuilder} that uses the given
* factory to create the underlying {@link HttpClient}.
* @param factory the factory to use
* @return a new {@link ReactorClientHttpRequestFactoryBuilder} instance
* @since 3.5.0
*/
public ReactorClientHttpRequestFactoryBuilder withHttpClientFactory(Supplier<HttpClient> factory) {
Assert.notNull(factory, "'factory' must not be null");
return new ReactorClientHttpRequestFactoryBuilder(getCustomizers(),
this.httpClientBuilder.withHttpClientFactory(factory));
}
/**
* Return a new {@link ReactorClientHttpRequestFactoryBuilder} that applies additional
* customization to the underlying {@link HttpClient}.

View File

@@ -17,6 +17,7 @@
package org.springframework.boot.http.client;
import java.time.Duration;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import javax.net.ssl.SSLException;
@@ -30,6 +31,7 @@ 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.ReactorResourceFactory;
import org.springframework.util.Assert;
import org.springframework.util.function.ThrowingConsumer;
@@ -43,16 +45,42 @@ import org.springframework.util.function.ThrowingConsumer;
*/
public final class ReactorHttpClientBuilder {
private final Supplier<HttpClient> factory;
private final UnaryOperator<HttpClient> customizer;
public ReactorHttpClientBuilder() {
this(UnaryOperator.identity());
this(HttpClient::create, UnaryOperator.identity());
}
private ReactorHttpClientBuilder(UnaryOperator<HttpClient> customizer) {
private ReactorHttpClientBuilder(Supplier<HttpClient> httpClientFactory, UnaryOperator<HttpClient> customizer) {
this.factory = httpClientFactory;
this.customizer = customizer;
}
/**
* Return a new {@link ReactorHttpClientBuilder} that uses the given
* {@link ReactorResourceFactory} to create the {@link HttpClient}.
* @param reactorResourceFactory the {@link ReactorResourceFactory} to use
* @return a new {@link ReactorHttpClientBuilder} instance
*/
public ReactorHttpClientBuilder withReactorResourceFactory(ReactorResourceFactory reactorResourceFactory) {
Assert.notNull(reactorResourceFactory, "'reactorResourceFactory' must not be null");
return new ReactorHttpClientBuilder(() -> HttpClient.create(reactorResourceFactory.getConnectionProvider()),
(httpClient) -> this.customizer.apply(httpClient).runOn(reactorResourceFactory.getLoopResources()));
}
/**
* Return a new {@link ReactorHttpClientBuilder} that uses the given factory to create
* the {@link HttpClient}.
* @param factory the factory to use
* @return a new {@link ReactorHttpClientBuilder} instance
*/
public ReactorHttpClientBuilder withHttpClientFactory(Supplier<HttpClient> factory) {
Assert.notNull(factory, "'factory' must not be null");
return new ReactorHttpClientBuilder(factory, this.customizer);
}
/**
* Return a new {@link ReactorHttpClientBuilder} that applies additional customization
* to the underlying {@link HttpClient}.
@@ -72,7 +100,7 @@ public final class ReactorHttpClientBuilder {
*/
public HttpClient build(HttpClientSettings settings) {
settings = (settings != null) ? settings : HttpClientSettings.DEFAULTS;
HttpClient httpClient = applyDefaults(HttpClient.create());
HttpClient httpClient = applyDefaults(this.factory.get());
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
httpClient = map.from(settings::connectTimeout).to(httpClient, this::setConnectTimeout);
httpClient = map.from(settings::readTimeout).to(httpClient, HttpClient::responseTimeout);

View File

@@ -19,11 +19,13 @@ package org.springframework.boot.http.client.reactive;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import reactor.netty.http.client.HttpClient;
import org.springframework.boot.http.client.ReactorHttpClientBuilder;
import org.springframework.http.client.ReactorResourceFactory;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -60,6 +62,30 @@ public final class ReactorClientHttpConnectorBuilder
return new ReactorClientHttpConnectorBuilder(mergedCustomizers(customizers), this.httpClientBuilder);
}
/**
* Return a new {@link ReactorClientHttpConnectorBuilder} that uses the given
* {@link ReactorResourceFactory} to create the underlying {@link HttpClient}.
* @param reactorResourceFactory the {@link ReactorResourceFactory} to use
* @return a new {@link ReactorClientHttpConnectorBuilder} instance
*/
public ReactorClientHttpConnectorBuilder withReactorResourceFactory(ReactorResourceFactory reactorResourceFactory) {
Assert.notNull(reactorResourceFactory, "'reactorResourceFactory' must not be null");
return new ReactorClientHttpConnectorBuilder(getCustomizers(),
this.httpClientBuilder.withReactorResourceFactory(reactorResourceFactory));
}
/**
* Return a new {@link ReactorClientHttpConnectorBuilder} that uses the given factory
* to create the underlying {@link HttpClient}.
* @param factory the factory to use
* @return a new {@link ReactorClientHttpConnectorBuilder} instance
*/
public ReactorClientHttpConnectorBuilder withHttpClientFactory(Supplier<HttpClient> factory) {
Assert.notNull(factory, "'factory' must not be null");
return new ReactorClientHttpConnectorBuilder(getCustomizers(),
this.httpClientBuilder.withHttpClientFactory(factory));
}
/**
* Return a new {@link ReactorClientHttpConnectorBuilder} that applies additional
* customization to the underlying {@link HttpClient}.

View File

@@ -19,6 +19,7 @@ package org.springframework.boot.http.client;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import io.netty.channel.ChannelOption;
@@ -26,9 +27,12 @@ import org.junit.jupiter.api.Test;
import reactor.netty.http.client.HttpClient;
import org.springframework.http.client.ReactorClientHttpRequestFactory;
import org.springframework.http.client.ReactorResourceFactory;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.spy;
/**
* Tests for {@link ReactorClientHttpRequestFactoryBuilder} and
@@ -44,6 +48,25 @@ class ReactorClientHttpRequestFactoryBuilderTests
super(ReactorClientHttpRequestFactory.class, ClientHttpRequestFactoryBuilder.reactor());
}
@Test
void withwithHttpClientFactory() {
boolean[] called = new boolean[1];
Supplier<HttpClient> httpClientFactory = () -> {
called[0] = true;
return HttpClient.create();
};
ClientHttpRequestFactoryBuilder.reactor().withHttpClientFactory(httpClientFactory).build();
assertThat(called).containsExactly(true);
}
@Test
void withReactorResourceFactory() {
ReactorResourceFactory resourceFactory = spy(new ReactorResourceFactory());
ClientHttpRequestFactoryBuilder.reactor().withReactorResourceFactory(resourceFactory).build();
then(resourceFactory).should().getConnectionProvider();
then(resourceFactory).should().getLoopResources();
}
@Test
void withCustomizers() {
List<HttpClient> httpClients = new ArrayList<>();

View File

@@ -18,18 +18,21 @@ package org.springframework.boot.http.client.reactive;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
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.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.ReactorHttpClientBuilder;
import org.springframework.http.client.ReactorResourceFactory;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.spy;
/**
* Tests for {@link ReactorClientHttpConnectorBuilder} and
@@ -44,6 +47,25 @@ class ReactorClientHttpConnectorBuilderTests
super(ReactorClientHttpConnector.class, ClientHttpConnectorBuilder.reactor());
}
@Test
void withwithHttpClientFactory() {
boolean[] called = new boolean[1];
Supplier<HttpClient> httpClientFactory = () -> {
called[0] = true;
return HttpClient.create();
};
ClientHttpConnectorBuilder.reactor().withHttpClientFactory(httpClientFactory).build();
assertThat(called).containsExactly(true);
}
@Test
void withReactorResourceFactory() {
ReactorResourceFactory resourceFactory = spy(new ReactorResourceFactory());
ClientHttpConnectorBuilder.reactor().withReactorResourceFactory(resourceFactory).build();
then(resourceFactory).should().getConnectionProvider();
then(resourceFactory).should().getLoopResources();
}
@Test
void withCustomizers() {
List<HttpClient> httpClients = new ArrayList<>();