Spring Once you use the Spring WebClient
at multiple places in your application, providing a unified configuration with copy-pasting, e.g., common headers to all places is cumbersome. The Spring WebClient provides a mechanism to customize all instances using the WebClientCustomizer
interface globally. This blog post demonstrates how to customize the Spring WebClient at a central place. Due to Spring Boot's autoconfiguration mechanism, there's almost nothing to set up in addition.
Spring WebClient Project Setup
The automatic registration of our WebClient
customizations is done by Spring Boot's autoconfiguration. Therefore the demo application uses spring-boot-starter-web
and spring-boot-start-webflux
.
If your application uses Spring WebFlux without Spring Boot, you can still follow this article. In such cases, make sure to mirror the autoconfiguration of Spring Boot inside your application.
For this example, the WebFlux dependency would be enough. As most projects out there still use the embedded Tomcat and some minor parts from WebFlux, we're using the following project setup:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.3</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>de.rieckpil.blog</groupId> <artifactId>spring-web-client-customizing</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-web-client-customizing</name> <description>Demo project for Spring Boot</description> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <!-- further test dependencies --> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> |
As part of the Spring Boot dependency, we get the following autoconfiguration class (WebClientAutoConfiguration
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | @Configuration(proxyBeanMethods = false) @ConditionalOnClass(WebClient.class) @AutoConfigureAfter({ CodecsAutoConfiguration.class, ClientHttpConnectorAutoConfiguration.class }) public class WebClientAutoConfiguration { private final WebClient.Builder webClientBuilder; public WebClientAutoConfiguration(ObjectProvider<WebClientCustomizer> customizerProvider) { this.webClientBuilder = WebClient.builder(); customizerProvider.orderedStream().forEach((customizer) -> customizer.customize(this.webClientBuilder)); } @Bean @Scope("prototype") @ConditionalOnMissingBean public WebClient.Builder webClientBuilder() { return this.webClientBuilder.clone(); } @Configuration(proxyBeanMethods = false) @ConditionalOnBean(CodecCustomizer.class) protected static class WebClientCodecsConfiguration { @Bean @ConditionalOnMissingBean @Order(0) public WebClientCodecCustomizer exchangeStrategiesCustomizer(List<CodecCustomizer> codecCustomizers) { return new WebClientCodecCustomizer(codecCustomizers); } } } |
This autoconfiguration exposes a pre-configured instance of WebClient.Builder
. Within the constructor of this class, we see the injection of ObjectProvider<WebClientCustomizer>
. This ObjectProvider
is capable of returning all object instances of the WebClientCustomizer
interface to customize the WebClient.Builder
.
While exposing this WebClient.Builder
instance in this autoconfiguration using @Bean
, we might wonder what the @Scope("prototype")
annotation is used. This overrides the default singleton scope to use the prototype scope. Spring won't share instances of these beans among the application like for singletons. For each injection point, a new instance is created and returned to the caller.
Defining Customizations with WebClientCustomizer
As we already saw how the autoconfiguration works internally, we now have to implement the WebClientCustomizer
.
First, we're creating a global customization to ensure all WebClients include the same User-Agent
HTTP header. In a distributed system architecture, this customization can help to understand communication patterns and to identify the origin of an HTTP request:
1 2 3 4 5 6 7 8 | @Component public class UserAgentCustomizer implements WebClientCustomizer { @Override public void customize(WebClient.Builder webClientBuilder) { webClientBuilder.defaultHeader("User-Agent", "MY-APPLICATION"); } } |
While implementing the WebClientCustomizer
interface, we get access to the WebClient.Builder
and can set multiple configurations like headers, cookies, exchange strategies, filters, and much more.
Next, let's provide a logging mechanism for all WebClient
instances. Therefore, we can create another customization:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | @Component public class ResponseLoggingCustomizer implements WebClientCustomizer { private static final Logger logger = LoggerFactory.getLogger(ResponseLoggingCustomizer.class); @Override public void customize(WebClient.Builder webClientBuilder) { webClientBuilder.filter(logResponse()); webClientBuilder.filter(logRequest()); } private ExchangeFilterFunction logResponse() { return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { logger.info("Response: {}", clientResponse.statusCode()); logger.info("--- Http Headers of Response: ---"); clientResponse.headers().asHttpHeaders() .forEach((name, values) -> values.forEach(value -> logger.info("{}={}", name, value))); return Mono.just(clientResponse); }); } private ExchangeFilterFunction logRequest() { return (clientRequest, next) -> { logger.info("Request: {} {}", clientRequest.method(), clientRequest.url()); logger.info("--- Http Headers of Request: ---"); clientRequest.headers().forEach(this::logHeader); return next.exchange(clientRequest); }; } private void logHeader(String name, List<String> values) { values.forEach(value -> logger.info("{}={}", name, value)); } } |
Please note the @Component
annotation on top of each of these two classes. This is required for Spring to pick it up while scanning the project for all available beans (aka. component scanning). Without this annotation, the ObjectProvider<WebClientCustomizer>
will we empty and won't know about our customization.
Using the Customized Spring WebClient
There are now basically two ways of using this pre-configured WebClient.
First, we can provide a central configuration with all our WebClient
instances. For an application that communicates with a stock and random data API, this might look like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 | @Configuration public class WebClientConfig { @Bean public WebClient stockApiClient(WebClient.Builder webClientBuilder) { return webClientBuilder.baseUrl("https://stock.api").build(); } @Bean public WebClient randomApiClient(WebClient.Builder webClientBuilder) { return webClientBuilder.baseUrl("https://random.api").build(); } } |
Our business logic can then request such an instance by its name, e.g. @Autowired WebClient stockApiClient
.
Second, we can also inject the WebClient.Builder
into our classes directly and construct a WebClient
instance there:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | @RestController @RequestMapping("/api/random") public class RandomDataController { private final WebClient webClient; public RandomDataController(WebClient.Builder webClientBuilder) { this.webClient = webClientBuilder.baseUrl("https://jsonplaceholder.typicode.com/").build(); } @GetMapping public JsonNode getRandomData() { return webClient .get() .uri("/todos") .retrieve() .bodyToMono(JsonNode.class) .block(); } } |
Please note: It's important to always inject and use the autoconfigured WebClient.Builder
from Spring Boot. If we manually construct a WebClient
using WebClient.builder()
, we won't get the customizations out of the box.
Once the WebClient
makes the remote call, we'll get the following output and can see all headers, including our custom User-Agent
:
1 2 3 4 5 6 7 8 9 | d.r.blog.ResponseLoggingCustomizer : Request: GET https://jsonplaceholder.typicode.com/todos d.r.blog.ResponseLoggingCustomizer : --- Http Headers of Request: --- d.r.blog.ResponseLoggingCustomizer : User-Agent=MY-APPLICATION d.r.blog.ResponseLoggingCustomizer : Response: 200 OK d.r.blog.ResponseLoggingCustomizer : --- Http Headers of Response: --- d.r.blog.ResponseLoggingCustomizer : Date=Wed, 04 Mar 2020 08:00:51 GMT d.r.blog.ResponseLoggingCustomizer : Content-Type=application/json; charset=utf-8 d.r.blog.ResponseLoggingCustomizer : Transfer-Encoding=chunked d.r.blog.ResponseLoggingCustomizer : Connection=keep-alive |
The source code for this Spring WebClient customization example is available on GitHub.
For more Spring WebClient related content, consider the following articles:
- Spring WebTestClient for efficient testing of your REST API
- Use Spring WebClient for RESTful communication
- Spring WebClient OAuth2 Integration for Spring WebFlux
- Expose Metrics of Spring WebClient using Spring Boot Actuator
Have fun customizing your Spring WebClient,
Phil