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 globally customize all instances using the WebClientCustomizer
interface. This blog post will demonstrate how you can customize the Spring WebClient at a central place and with the help of Spring Boot you almost have 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. You just have to mirror the configuration 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, I'm following this 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.2.2.RELEASE</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> |
With Spring Boot we get the following autoconfiguration (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, you 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
, you might wonder what the @Scope("prototype")
annotation is used. This overrides the default singleton scope to use the prototype scope. Beans of this type won't be shared among the codebase as a single instance, but for each injection point, a new instance is returned.
Defining customizations with WebClientCustomizer
As we already saw how the autoconfiguration works internally, we now have to implement the WebClientCustomizer
.
First, I'm creating a global customization, so that all WebClients include the same User-Agent
HTTP header:
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, you 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. Without this annotation, the ObjectProvider<WebClientCustomizer>
you saw in the last chapter won't know about your customization.
Make use of the customized Spring WebClient
There are now basically two ways of using this pre-configured WebClients.
First, you can provide a central configuration with all your WebClient
instances you use within your project. 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(); } } |
Your business logic can then request such an instance by its name, e.g. @Autowired WebClient stockApiClient
.
Second, you can also inject the WebClient.Builder
into your 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 you manually construct a WebClient
using WebClient.builder()
, you won't get the customizations out-of-the-box.
Once the WebClient
makes the remote call, you'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 |
You can find the source code for this example on GitHub.
For more Spring WebClient related content, follow the links below:
- 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.