As the Spring Framework team won't add new features to the RestTemplate
, the general recommendation is to start using the Spring WebFlux WebClient. Besides the reactive and non-blocking nature of the WebClient
, you can seamlessly include it to your existing (blocking) application. Apart from learning the basics about the reactive types Mono
and Flux
, it might be difficult to understand .retrieve()
and .exchange()
when using the Spring WebClient for the first time. With this blog post, I want to give you an overview of the Spring WebClient functions exchange and retrieve their differences, and when to use them.
TL;DR: Always try to use the WebClient .retrieve()
. If you need more fine-grain control, use the .exchange()
method but understand your additional responsibilities of always releasing the body.
UPDATE: As of Spring Framework 5.3, using .exchange()
is deprecated due to potential memory and connection leaks. Prefer .exchangeToMono()
, .exchangeToFlux()
, or .retrieve()
instead.
Spring Boot project setup for Spring WebClient
To compare both methods I'm using a sample Spring Boot application containing both the Web and WebFlux starter. In such scenarios where both Web Starters are available on the classpath, the autoconfiguration mechanism of Spring Boot will start the embedded Tomcat (non-reactive).
Nevertheless, the application can still use parts of WebFlux, but the overall execution is blocking. Hence all code examples use the WebClient
in a non-reactive fashion. Everything further mentioned also applies to scenarios where you use the WebClient
in a full-stack reactive application.
The application uses Java 11 and Spring Boot 2.3:
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 45 46 | <?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.3.0.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>de.rieckpil.blog</groupId> <artifactId>spring-web-client-exchange-retrieve</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-web-client-exchange-retrieve</name> <description>Spring WebClient exchange vs. retrieve</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> <!-- Test dependencies --> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> |
Spring WebClient configuration
First, let's configure a WebClient
bean, that we'll use throughout the different examples.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | @Configuration public class WebClientConfig { private static final int TIMEOUT_IN_SECONDS = 2; @Bean public WebClient jsonPlaceholderWebClient(WebClient.Builder webClientBuilder) { TcpClient tcpClient = TcpClient.create() .option(CONNECT_TIMEOUT_MILLIS, TIMEOUT_IN_SECONDS * 1000) .doOnConnected(connection -> connection .addHandlerLast(new ReadTimeoutHandler(TIMEOUT_IN_SECONDS)) .addHandlerLast(new WriteTimeoutHandler(TIMEOUT_IN_SECONDS))); return webClientBuilder .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient))) .baseUrl("https://jsonplaceholder.typicode.com/") .build(); } } |
Besides configuring the base URL, we're setting different timeouts while creating our WebClient
bean. There is nothing worse than long-running operations in a distributed system. For this showcase, I'm using a placeholder REST API that allows us to fetch and create different entities.
The injected WebClient.Builder
is autoconfigured by Spring Boot for us and in general good practice to use this for creating WebClient
beans.
Furthermore, there is not different configuration for the WebClient
when it comes to .retrieve()
or .exchange()
. With the bean definition above we can use both methods.
Fetching data with retrieve from Spring WebClient
Let's start with a trivial example: fetching data with HTTP GET.
Using .retrieve()
we can achieve this operation with the following code:
1 2 3 4 5 6 7 | public JsonNode getTodos() { return this.jsonPlaceholderWebClient.get() .uri("/todos") .retrieve() .bodyToMono(JsonNode.class) .block(); } |
The internal return type of .retrieve()
is ResponseSpec
which is part of the WebClient
interface. The ResponseSpec
offers several methods to proceed after sending the HTTP request:
- wrapping the response body to reactive types using:
.bodyToMono()
,.bodyToFlux()
- wrapping the response inside a Mono of the well-known (from using
RestTemplate
or your controller layer)ResponseEntity
with:.toEntity()
,.toEntityList()
,.toBodilessEntity
( - register callback handlers on different HTTP response status:
.onStatus()
on.rawStatus()
Here we are converting the HTTP response to the Jackson JsonNode
class and with calling .block()
await the response to use it in our non-reactive application.
Fetching data with exchange from Spring WebClient
The same operation looks like the following when using .exchange()
:
1 2 3 4 5 6 7 | public JsonNode getTodos() { return this.jsonPlaceholderWebClient.get() .uri("/todos") .exchange() .flatMap(clientResponse -> clientResponse.bodyToMono(JsonNode.class)) .block(); } |
Until calling .exchange()
everything is similar compared to using .retrieve()
. The main difference is the return type of the .exchange()
method. As a result, you have access to Mono<ClientResponse>
instead of ResponseSpec
.
Having this ClientResponse
wrapped into a Mono
allows us to use all available methods of Mono
. In the example above I'm flattening the result and wrap the response to the exact data type like we did before.
The Javadoc of ClientResponse
contains an important note saying:
NOTE: When using a
ClientResponse
through the WebClientexchange()
method, you have to make sure that the body is consumed or released by using one of the following methods: body(BodyExtractor), bodyToMono(Class), …
So here we are warned to actively release the body of the HTTP response when using .exchange()
. Not doing so can result in memory leaks as the Javadoc of .exchange()
states the following:
NOTE: Unlike
retrieve()
, when usingexchange()
, it is the responsibility of the application to
consume any response content regardless of the scenario (success, error, unexpected data, etc). Not doing so can cause a memory leak. SeeClientResponse
for a list of all the available options for consuming the body
Apart from the body extraction function (similar to what's available on ResponseSpec
): .bodyToMono()
, .toEntity()
, etc. we have additional functions to e.g. access the HTTP response headers. This allows you to check the HTTP response headers or the status code first, before deciding if and how to convert the body.
1 2 3 4 5 6 7 8 9 | // ... .exchange() .flatMap(clientResponse -> { if(clientResponse.headers().header("X-My-Custom-Header").size() > 0) { return clientResponse.bodyToMono(JsonNode.class); }else { return clientResponse.toBodilessEntity(); } }) |
Accessing fields of the HTTP response
Let's add another example for both methods and see how we can react to different HTTP response status codes.
Using .retrieve()
, we can specify a function using .onStatus()
to define the behavior of different error status codes. By default, every status code >= 400 (Bad Request) is internally mapped to a WebClientResponseException
. Using the following setup, we can override this and specify our custom logic:
1 2 3 4 5 6 7 | return this.jsonPlaceholderWebClient.get() .uri("/todos/{id}", id) .retrieve() .onStatus(HttpStatus::is4xxClientError, response -> response.rawStatusCode() == 418 ? Mono.empty() : Mono.error(new RuntimeException("Error"))) .onStatus(HttpStatus::is5xxServerError, response -> Mono.error(new RuntimeException("Error"))) .bodyToMono(JsonNode.class) .block(); |
This allows us to only define the behavior for error status codes, as our provided function has to return Mono<? extends Throwable>
. But what if we e.g. want to check that the response code is 201 (when creating entities) and add custom logic if not?
As we can extract the HTTP body to a ResponseEntity
, we can write the following code for such scenarios:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public boolean createTodo(JsonNode payload) { ResponseEntity<JsonNode> response = this.jsonPlaceholderWebClient .post() .uri("/todos") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .bodyValue(payload) .retrieve() .toEntity(JsonNode.class) .block(); response.getHeaders().forEach((key, value) -> System.out.println(key + ":" + value)); if (response.getStatusCodeValue() == 201) { return !response.getHeaders().get(HttpHeaders.LOCATION).isEmpty(); } else { return false; } } |
In the scenario above we had to define the actual payload type (JsonNode
) before we could access the HTTP header and the status code.
The same operation with .exchange()
instead can look like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public boolean createTodo(JsonNode payload) { ClientResponse response = this.jsonPlaceholderWebClient .post() .uri("/todos") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .bodyValue(payload) .exchange() .block(); if (response.statusCode().value() == 201) { return !response.headers().header(HttpHeaders.LOCATION).isEmpty(); } else { return false; } } |
Using .exchange()
in this example frees us from defining the payload type first.
Summary of the exchange and retrieve comparison
The examples above should now give you a first impression of both methods and their differences. Basically .retrieve()
is the method you should aim for using most of the time. The Spring WebClient exchange method provides more control than the retrieve method but makes you responsible for consuming the body in every scenario.
For most of your use cases .retrieve()
should be your go-to solution and if you need access to headers and the status code, you can still convert the response to a ResponseEntity
. A good reason to use .exchange()
is to check the response status and headers before deciding how or if to consume the response.
Technically speaking, .retrieve()
is a shortcut to using .exchange()
and decoding the response body through ClientResponse
, as the implementation of .retrieve()
is the following:
1 2 3 4 | @Override public ResponseSpec retrieve() { return new DefaultResponseSpec(exchange(), this::createRequest); } |
Furthermore, there is no difference in preparing the HTTP request (e.g setting URL, HTTP method, or payload) when it comes to using both methods.
The Source code for these Spring WebClient examples is available on GitHub.
Have fun using both exchange and retrieve of the Spring WebClient,
Phil