From 07912f763a8e1f20f7a01753169a25dca19768bb Mon Sep 17 00:00:00 2001 From: Christian Kumpe <3316684+ckumpe@users.noreply.github.com> Date: Mon, 11 Sep 2023 14:19:10 +0200 Subject: [PATCH] Provide the ability to customize the WebClient instance used in the HalResourceLoader --- .../spring/api/HttpClientCustomizer.java | 17 +++++ .../rhyme/spring/api/WebClientProvider.java | 18 ++++++ .../impl/SpringRhymeAutoConfiguration.java | 63 ++++++++++++++++--- .../rhyme/spring/impl/WebClientSupport.java | 34 +++------- .../spring/impl/WebClientSupportTest.java | 8 ++- 5 files changed, 104 insertions(+), 36 deletions(-) create mode 100644 integration/spring/src/main/java/io/wcm/caravan/rhyme/spring/api/HttpClientCustomizer.java create mode 100644 integration/spring/src/main/java/io/wcm/caravan/rhyme/spring/api/WebClientProvider.java diff --git a/integration/spring/src/main/java/io/wcm/caravan/rhyme/spring/api/HttpClientCustomizer.java b/integration/spring/src/main/java/io/wcm/caravan/rhyme/spring/api/HttpClientCustomizer.java new file mode 100644 index 00000000..d9944269 --- /dev/null +++ b/integration/spring/src/main/java/io/wcm/caravan/rhyme/spring/api/HttpClientCustomizer.java @@ -0,0 +1,17 @@ +package io.wcm.caravan.rhyme.spring.api; + +import reactor.netty.http.client.HttpClient; + +/** + * Callback interface that can be used to customize a {@link reactor.netty.http.client.HttpClient}. E.g., to add a proxy configuration. + */ +@FunctionalInterface +public interface HttpClientCustomizer { + + /** + * Callback to customize a {@link reactor.netty.http.client.HttpClient} instance. + * @param httpClient the client to customize + */ + void customize(HttpClient httpClient); + +} diff --git a/integration/spring/src/main/java/io/wcm/caravan/rhyme/spring/api/WebClientProvider.java b/integration/spring/src/main/java/io/wcm/caravan/rhyme/spring/api/WebClientProvider.java new file mode 100644 index 00000000..94e44607 --- /dev/null +++ b/integration/spring/src/main/java/io/wcm/caravan/rhyme/spring/api/WebClientProvider.java @@ -0,0 +1,18 @@ +package io.wcm.caravan.rhyme.spring.api; + +import java.net.URI; + +import org.springframework.web.reactive.function.client.WebClient; + +/** + * Callback interface to provide an instance of {@link WebClient} for the given uri. + */ +@FunctionalInterface +public interface WebClientProvider { + + /** + * Returns an instance of {@link WebClient} for the given uri. + */ + WebClient webClientForUri(URI uri); + +} diff --git a/integration/spring/src/main/java/io/wcm/caravan/rhyme/spring/impl/SpringRhymeAutoConfiguration.java b/integration/spring/src/main/java/io/wcm/caravan/rhyme/spring/impl/SpringRhymeAutoConfiguration.java index d1902215..190e997f 100644 --- a/integration/spring/src/main/java/io/wcm/caravan/rhyme/spring/impl/SpringRhymeAutoConfiguration.java +++ b/integration/spring/src/main/java/io/wcm/caravan/rhyme/spring/impl/SpringRhymeAutoConfiguration.java @@ -19,15 +19,23 @@ */ package io.wcm.caravan.rhyme.spring.impl; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; import io.wcm.caravan.rhyme.api.client.HalResourceLoaderBuilder; import io.wcm.caravan.rhyme.api.spi.HalResourceLoader; +import io.wcm.caravan.rhyme.api.spi.HttpClientSupport; +import io.wcm.caravan.rhyme.spring.api.HttpClientCustomizer; import io.wcm.caravan.rhyme.spring.api.SpringRhyme; +import io.wcm.caravan.rhyme.spring.api.WebClientProvider; +import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; /** * Registers all components from the impl package, and provides default implementations for spring beans required by @@ -46,25 +54,64 @@ class SpringRhymeAutoConfiguration { */ @Bean @ConditionalOnMissingBean - HalResourceLoader halResourceLoader() { + HalResourceLoader halResourceLoader(HalResourceLoaderBuilder halResourceBuilder) { - return halResourceBuilder() + return halResourceBuilder .withMemoryCache() .build(); } /** - * Provides a {@link HalResourceLoaderBuilder} that is already pre-configured to use a {@link WebClientSupport} + * Provides a {@link HalResourceLoaderBuilder} that is pre-configured with the given {@link HttpClientSupport} * @return a {@link HalResourceLoaderBuilder} that can be further customised before a {@link HalResourceLoader} is - * built + * built */ @Bean @ConditionalOnMissingBean - HalResourceLoaderBuilder halResourceBuilder() { + HalResourceLoaderBuilder halResourceBuilder(HttpClientSupport httpClientSupport) { + return HalResourceLoaderBuilder.create() + .withCustomHttpClient(httpClientSupport); + } - WebClientSupport client = new WebClientSupport(); + /** + * Provides a {@link WebClientSupport} instance with the given {@link WebClient} + */ + @Bean + @ConditionalOnMissingBean + WebClientSupport webClientSupport(WebClientProvider webClientProvider) { + return new WebClientSupport(webClientProvider); + } - return HalResourceLoaderBuilder.create() - .withCustomHttpClient(client); + /** + * Simply provides a static instance of {@link WebClient} for all URIs created with the provided {@link WebClient.Builder}. + */ + @Bean + @Scope("prototype") + @ConditionalOnMissingBean + WebClientProvider webClientProvider(WebClient.Builder webClientBuilder) { + WebClient webClient = webClientBuilder.build(); + return uri -> webClient; } + + /** + * Provides a default instance for a {@link WebClient.Builder} with a configured {@link ConnectionProvider}. + */ + @Bean + @Scope("prototype") + @ConditionalOnMissingBean + WebClient.Builder defaultRhymeWebClientBuilder(ObjectProvider httpClientCustomizerProvider) { + ConnectionProvider connectionProvider = ConnectionProvider + .builder(SpringRhymeAutoConfiguration.class.getSimpleName()) + .maxConnections(5000) + .build(); + + HttpClient httpClient = HttpClient.create(connectionProvider); + + httpClientCustomizerProvider.orderedStream().forEach(customizer -> customizer.customize(httpClient)); + + return WebClient.builder() + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024)) + .clientConnector(new ReactorClientHttpConnector(httpClient)); + } + } diff --git a/integration/spring/src/main/java/io/wcm/caravan/rhyme/spring/impl/WebClientSupport.java b/integration/spring/src/main/java/io/wcm/caravan/rhyme/spring/impl/WebClientSupport.java index f6301091..530cd9bc 100644 --- a/integration/spring/src/main/java/io/wcm/caravan/rhyme/spring/impl/WebClientSupport.java +++ b/integration/spring/src/main/java/io/wcm/caravan/rhyme/spring/impl/WebClientSupport.java @@ -24,39 +24,23 @@ import org.apache.commons.lang3.ObjectUtils; import org.springframework.http.ResponseEntity; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; import io.wcm.caravan.rhyme.api.spi.HttpClientCallback; import io.wcm.caravan.rhyme.api.spi.HttpClientSupport; +import io.wcm.caravan.rhyme.spring.api.WebClientProvider; import reactor.core.publisher.Mono; -import reactor.netty.http.client.HttpClient; -import reactor.netty.resources.ConnectionProvider; -final class WebClientSupport implements HttpClientSupport { +public final class WebClientSupport implements HttpClientSupport { - private final ConnectionProvider connectionProvider = ConnectionProvider - .builder(WebClientSupport.class.getSimpleName()) - .maxConnections(5000) - .build(); + private final WebClientProvider webClientProvider; - private WebClient createDefaultWebClient() { - - HttpClient httpClient = HttpClient.create(connectionProvider); - - return WebClient.builder() - .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024)) - .clientConnector(new ReactorClientHttpConnector(httpClient)) - .build(); - } + public WebClientSupport(WebClientProvider webClientProvider) {this.webClientProvider = webClientProvider;} @Override public void executeGetRequest(URI uri, HttpClientCallback callback) { - - WebClient client = createDefaultWebClient(); - - client.get().uri(uri).retrieve() + webClientProvider.webClientForUri(uri) + .get().uri(uri).retrieve() // any 200 responses will be parsed as a string and forwarded to the callback .toEntity(byte[].class).doOnSuccess(entity -> handleOkResponse(callback, entity)) // any responses with error status should be handled specifically, as we want to @@ -66,13 +50,13 @@ public void executeGetRequest(URI uri, HttpClientCallback callback) { // any other exceptions thrown during the request or while handling the response // should be caught as well .doOnError(callback::onExceptionCaught) - // finally subscribe so that the request is actually executed + // finally, subscribe so that the request is actually executed .subscribe(); } private void handleOkResponse(HttpClientCallback callback, ResponseEntity entity) { - callback.onHeadersAvailable(entity.getStatusCodeValue(), entity.getHeaders()); + callback.onHeadersAvailable(entity.getStatusCode().value(), entity.getHeaders()); byte[] body = ObjectUtils.defaultIfNull(entity.getBody(), new byte[0]); @@ -82,7 +66,7 @@ private void handleOkResponse(HttpClientCallback callback, ResponseEntity> handleErrorResponse(HttpClientCallback callback, WebClientResponseException ex) { - callback.onHeadersAvailable(ex.getRawStatusCode(), ex.getHeaders()); + callback.onHeadersAvailable(ex.getStatusCode().value(), ex.getHeaders()); callback.onBodyAvailable(new ByteArrayInputStream(ex.getResponseBodyAsByteArray())); diff --git a/integration/spring/src/test/java/io/wcm/caravan/rhyme/spring/impl/WebClientSupportTest.java b/integration/spring/src/test/java/io/wcm/caravan/rhyme/spring/impl/WebClientSupportTest.java index 22b2951a..3b52bfd2 100644 --- a/integration/spring/src/test/java/io/wcm/caravan/rhyme/spring/impl/WebClientSupportTest.java +++ b/integration/spring/src/test/java/io/wcm/caravan/rhyme/spring/impl/WebClientSupportTest.java @@ -22,6 +22,7 @@ import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; +import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientRequestException; import io.wcm.caravan.rhyme.api.exceptions.HalApiClientException; @@ -29,18 +30,19 @@ import io.wcm.caravan.rhyme.testing.client.AbstractHalResourceLoaderTest; /** - * Runs a set of tests for the {@link WebClientHalResourceLoader} against a Wiremock server + * Runs a set of tests for the {@link WebClientSupport} based HalResourceLoader against a Wiremock server */ class WebClientSupportTest extends AbstractHalResourceLoaderTest { @Override protected HalResourceLoader createLoaderUnderTest() { - return HalResourceLoader.create(new WebClientSupport()); + WebClient webClient = WebClient.create(); + return HalResourceLoader.create(new WebClientSupport(uri -> webClient)); } // after upgrading to Spring Boot 2.5.8, WebClient is handling a few edge cases differently, - // so for now we are overriding the text with updated expectations + // so for now we are overriding the test with updated expectations @Override @Test