Skip to content

Commit

Permalink
Merge pull request #49 from wcm-io-caravan/feature/Provide-customizab…
Browse files Browse the repository at this point in the history
…le-WebClient

Provide the ability to customize the WebClient instance used in the HalResourceLoader
  • Loading branch information
ckumpe authored Sep 13, 2023
2 parents ffb73a5 + 07912f7 commit 50cfa4b
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -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);

}
Original file line number Diff line number Diff line change
@@ -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);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<HttpClientCustomizer> 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));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<byte[]> entity) {

callback.onHeadersAvailable(entity.getStatusCodeValue(), entity.getHeaders());
callback.onHeadersAvailable(entity.getStatusCode().value(), entity.getHeaders());

byte[] body = ObjectUtils.defaultIfNull(entity.getBody(), new byte[0]);

Expand All @@ -82,7 +66,7 @@ private void handleOkResponse(HttpClientCallback callback, ResponseEntity<byte[]
private Mono<ResponseEntity<byte[]>> handleErrorResponse(HttpClientCallback callback,
WebClientResponseException ex) {

callback.onHeadersAvailable(ex.getRawStatusCode(), ex.getHeaders());
callback.onHeadersAvailable(ex.getStatusCode().value(), ex.getHeaders());

callback.onBodyAvailable(new ByteArrayInputStream(ex.getResponseBodyAsByteArray()));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,27 @@
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;
import io.wcm.caravan.rhyme.api.spi.HalResourceLoader;
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
Expand Down

0 comments on commit 50cfa4b

Please sign in to comment.