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
How can I add basic authentication after doing a POST (Authentication) or before running the application?
You can configure basic authentication with a filter
.filter(ExchangeFilterFunctions.basicAuthentication("rieckpil", UUID.randomUUID().toString()))
when creating theWebClient
. You can find further examples on how to use filters here.Thanks for this detailed article.
Do you know a way to get the customizations out-of-the-box without injecting the builder?
I have a webClient generated by open-api so I can’t inject the builder by myself. I would force the open-api WebClient creation to inherit the auto-configurations.
Thanks
Hi Val,
you can find the “behind-the-scenes” inside Spring’s
WebClientAutoConfiguration
class. Take a look at the@Bean
definition that creates theWebClient.Builder
. This injects a list of customizers (ObjectProvider customizerProvider
) to apply them to the builder. You could inject the same list to customize yourWebClient
.Let me know if that helps,
Philip
Hi rieckpil,
Thanks for your answer!
I’ll try to explain what I understand as part of this GitHub issue.
Hi Val,
as the functionality of my blog’s comment section is quite limited when it comes to paste large code examples and seamlessly interact, I’ve moved your question over to a GitHub issue. Make sure to at least add a comment on the issue so that you’ll be notified on new discussion.
I hope this works for you and let’s move the discussion to Github.
Kind regards,
Philip
[…] auto-configured builder customizes the WebClient to, among other things, emit metrics about the HTTP response codes and response […]
Thank you. Is there a way to get one instance of WebClient customized and second instance uncustomized?
yes, to get an uncustomized instance, you can simply instantiate the WebClient yourself (aka. don’t inject the builder).
Is it possible to customize WebClient per http request?
I have micro services that authenticate requests using cookies. So at the point of entry, I want to extract the cookie and add it as default cookie to the WebClient builder, so that all future requests will inherit it and use it when interacting with other micro services.
Is that possible.
The current implementation calls the customizer at application startup, but I want it to be called at the point of http request.
Thanks in advance.
Hi Adindu,
yes, you can construct a
WebClient
instance using theWebClientBuilder
upon each request. Simply instantiate a new instance whenever you need to change your cookie.Kind regards,
Philip