RESTful communication is the de-facto standard for interchanging data in a microservice-based environment. Usually, every participating microservice offers different parts of the application's domain in a RESTful way and calls other microservices to gather data for e.g. a different part of the application's domain. Resilience and efficient data interchange is, therefore, a key factor when it comes to the success of the whole application. With Spring we usually made use of the RestTemplate
to perform HTTP requests, but can now also use the Spring WebClient.
With the release of Spring Framework 5.0, the documentation of the RestTemplate
says the following:
NOTE: As of 5.0, the non-blocking, reactive
org.springframework.web.reactive.client.WebClient
offers a modern alternative to theRestTemplate
with efficient support for both sync and async, as well as streaming scenarios. TheRestTemplate
will be deprecated in a future version and will not have major new features added going forward. See the WebClient section of the Spring Framework reference documentation for more details and example code.
To be future-ready, your Spring-based application should migrate to the reactive and non-blocking Spring WebClient
for both its async & sync HTTP communication.
In this blog post, I'll demonstrate how to work with the WebClient
class to master common tasks like HTTP GET/POST requests, configuration, filtering requests, and testing. For the demo application, I'll use Java 11 and Spring Boot 2.3.
Configure the WebClient
For utilizing the WebClient
, you need the following dependency in your Spring Boot project:
1 2 3 4 | <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> |
The WebClient
internally delegates to an HTTP client library (by default Reactor Netty), but others can be plugged in through a ClientHttpConnector
. More information about the WebClient
is available here.
When it comes to configuring resilient HTTP clients, connection/read/write timeouts are important to avoid long-running tasks. In addition, HTTP headers and cookies are essential for e.g. authentication or content-negotiation. With WebClient
, you can make use of a builder to configure this:
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 | @Component public class SimpleWebClientConfiguration { private static final String BASE_URL = "https://jsonplaceholder.typicode.com"; private static final Logger logger = LoggerFactory.getLogger(SimpleApiClient.class); @Bean public WebClient defaultWebClient() { var tcpClient = TcpClient.create() .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2_000) .doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(2)) .addHandlerLast(new WriteTimeoutHandler(2))); return WebClient.builder() .baseUrl(BASE_URL) .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient))) .defaultCookie("cookieKey", "cookieValue", "teapot", "amsterdam") .defaultCookie("secretToken", UUID.randomUUID().toString()) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) .defaultHeader(HttpHeaders.USER_AGENT, "I'm a teapot") .build(); } } |
Make an HTTP GET request with Spring WebClient
Once your WebClient
is configured for a specific baseUrl, you can start performing HTTP requests. As the internal WebClient
architecture is designed for reactive and non-blocking applications, you either have to call .block()
or rewrite your codebase to accept Mono<T>
and Flux<T>
as method return types.
A simple sync HTTP GET request with our previously configured WebClient
looks like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @Service public class SimpleApiClient { private final WebClient defaultWebClient; public SimpleApiClient(WebClient defaultWebClient) { this.defaultWebClient = defaultWebClient; } public JsonNode getTodoFromAPI() { return this.defaultWebClient.get().uri("/todos/1") .retrieve() .bodyToMono(JsonNode.class) .block(); } } |
For actually performing an HTTP request, you can either use .retrieve()
or .exchange()
whereby the first method is the easiest way and .exchange()
offers more control (have a look at this blog post or the documentation to understand the difference in detail). For simplification, I'll use .retrieve()
in all examples.
Reacting on different HTTP response codes
By default, HTTP responses with 4xx (e.g. 404 for not found) or 5xx (e.g. 500 service unavailable) status codes result in an WebClientResponseException
. For modifying the way how errors are handled, you can use the onStatus
method of Spring's WebClient
to customize the resulting exception:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | @Service public class SimpleApiClient { private final WebClient defaultWebClient; public SimpleApiClient(WebClient defaultWebClient) { this.defaultWebClient = defaultWebClient; } public JsonNode getTodoFromAPI() { return this.defaultWebClient.get().uri("/todos/1") .retrieve() .onStatus(HttpStatus::is4xxClientError, response -> { System.out.println("4xx error"); return Mono.error(new RuntimeException("4xx")); }) .onStatus(HttpStatus::is5xxServerError, response -> { System.out.println("5xx error"); return Mono.error(new RuntimeException("5xx")); }) .bodyToMono(JsonNode.class) .block(); } } |
Make an HTTP POST request
Performing HTTP POST request with the WebClient
is as simple as the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @Service public class SimpleApiClient { private final WebClient defaultWebClient; public SimpleApiClient(WebClient defaultWebClient) { this.defaultWebClient = defaultWebClient; } public JsonNode postToTodoAPI() { return this.defaultWebClient .post() .uri("/todos") .body(BodyInserters.fromValue("{ \"title\": \"foo\", \"body\": \"bar\", \"userId\": \"1\"}")) .retrieve() .bodyToMono(JsonNode.class) .block(); } } |
To put the request body in place you can use the BodyInserters
utility class and add any kind of Object
, FormData
, Publisher
, MultipartData
, Resource
and much more.
Retry configuration on failure
For a more resilient architecture, you can configure retries. You can use the public methods of Mono
and Flux
for this. Let's say we want to retry two times (max. three HTTP calls) and add a small delay between each retry:
1 2 3 4 5 6 7 | public JsonNode getRetryTodoFromAPI() { return this.defaultWebClient.get().uri("/todos/1") .retrieve() .bodyToMono(JsonNode.class) .retryWhen(Retry.fixedDelay(2, Duration.ofMillis(300))) .block(); } |
Furthermore, you can combine the retry mechanism with a timeout from Mono
or Flux
to provide a fallback value:
1 2 3 4 5 6 7 8 | public JsonNode getRetryTodoFromAPI() { return this.defaultWebClient.get().uri("/todos/1") .retrieve() .bodyToMono(JsonNode.class) .retryWhen(Retry.max(5)) .timeout(Duration.ofSeconds(2), Mono.just(objectMapper.createObjectNode().put("message", "fallback"))) .block(); } |
Configure filters for the Spring WebClient
If you have business logic or a configuration setup which should be applied to every HTTP request and response made with the WebClient
, you can configure and chain multiple filters.
Logging the request/response might be such a requirement or applying authentication to the request. This is configured during the WebClient
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 45 46 47 48 | @Component public class SimpleWebClientConfiguration { private static final String BASE_URL = "https://jsonplaceholder.typicode.com"; private static final Logger logger = LoggerFactory.getLogger(SimpleApiClient.class); @Bean public WebClient defaultWebClient() { var tcpClient = TcpClient.create() .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2_000) .doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(2)) .addHandlerLast(new WriteTimeoutHandler(2))); return WebClient.builder() .baseUrl(BASE_URL) .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient))) .filter(ExchangeFilterFunctions.basicAuthentication("rieckpil", UUID.randomUUID().toString())) .filter(logRequest()) .filter(logResponse()) .build(); } private ExchangeFilterFunction logRequest() { return (clientRequest, next) -> { logger.info("Request: {} {}", clientRequest.method(), clientRequest.url()); logger.info("--- Http Headers: ---"); clientRequest.headers().forEach(this::logHeader); logger.info("--- Http Cookies: ---"); clientRequest.cookies().forEach(this::logHeader); return next.exchange(clientRequest); }; } private ExchangeFilterFunction logResponse() { return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { logger.info("Response: {}", clientResponse.statusCode()); clientResponse.headers().asHttpHeaders() .forEach((name, values) -> values.forEach(value -> logger.info("{}={}", name, value))); return Mono.just(clientResponse); }); } private void logHeader(String name, List<String> values) { values.forEach(value -> logger.info("{}={}", name, value)); } } |
You can also provide global customizations for your WebClient
instances using the WebClientCustomizer interface.
Testing with WebTestClient
For efficient testing, you can make use of WebTestClient
during your integration tests. This client offers the same functionality as the normal WebClient
and convenient methods for assertions and expectations in addition.
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 | package de.rieckpil.blog; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.reactive.function.BodyInserters; @ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class SimpleApiClientTest { @Autowired private WebTestClient webTestClient; @Test public void testGetTodosAPICall() { this.webTestClient .get() .uri("https://jsonplaceholder.typicode.com/todos/1") .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk() .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE) .expectBody() .jsonPath("$.title").isNotEmpty() .jsonPath("$.userId").isNotEmpty() .jsonPath("$.completed").isNotEmpty(); } } |
I covered the usage of WebTestClient
with a dedicated blog post.
Accessing OAuth2 protected resources with Spring WebClient
With Spring Security we get a full integration for OAuth2. Have a look at the following blog posts where I demonstrate how to enable the OAuth2 integration with Spring WebClient
:
- Spring WebClient OAuth2 Integration for Spring Web (Servlet)
- Spring WebClient OAuth2 Integration for Spring WebFlux
Furthermore, if you use Spring Boot Actuator, you can expose metrics of your WebClient instances automatically.
You can find the full source code for all examples on GitHub. If you want to achieve the same with Jakarta EE, have a look at the JAX-RS Client
or the MicroProfile RestClient.
Have fun using the new Spring WebClient
,
Phil
[…] one of my recent blog posts, I presented Spring’s WebClient for RESTful communication. With Java EE we can utilize the […]
Hello Philip
I have a doubt:
I’d like to use WebClient instead of RestTemplate, but my solution is not SpringBoot but Spring default.
How do I add only WebClient dependencies and use?
Hey Fernando,
the
WebClient
is part of the WebFlux Spring dependency. You can pull this in your project without using Spring Boot. If you are using Maven, you can use the following import:<dependency<
<groupIdorg.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
Make sure to align the version with the Spring version you have in use.
[…] Java is enough for simple use cases, it might be helpful to use Spring Framework features (e.g the WebClient, data access, etc.). With Spring Cloud Function you can achieve this and use the AWS adapter to […]
[…] Our sample project is a basic Spring Boot application. We’ll include both starters for Web and WebFlux. Hence Spring Boot autoconfigures the embedded Tomcat for us while we are able to use parts from Spring WebFlux like the WebClient. […]
[…] 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 […]
One question, Why do I get the illegal StateException: The underlying HTTP client completed without emitting a response when using the web client? When I use block it works correctly, any idea why it could be happening?
Hey Daniel,
hard to tell without seeing the actual code. Could you create a question on StackOverflow and send me the link?
Kind regards,
Philip
[…] the WebClient, Spring provides a WebTestClient for testing purposes. The API of this class is similar to the […]