Spring Developers, Did you ever felt the need for an asynchronous/non-blocking HTTP client with a fluent functional style API that was easy to use and efficient?
If Yes, then I welcome you to this article about WebClient, the new reactive HTTP client introduced in Spring 5.
How to use WebClient
WebClient is part of Spring 5’s reactive web framework called Spring WebFlux. To use WebClient, you need to include the spring-webflux
module in your project.
Add Dependency in an existing Spring Boot project
If you have an existing Spring Boot project, you can add the spring-webflux
module by adding the following dependency in the pom.xml
file -
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
Create a new project from Scratch
If you’re creating a project from scratch, then you can generate a starter project with spring-webflux
module from the Spring Initializr website -
- Go to http://start.spring.io.
- Set Artifact and Group to
webclient-demo
. - Set Package to
com.example.webclientdemo
. - Add Reactive Web, and Validation dependencies.
- Click Generate to generate and download the project.
Consuming Remote APIs using WebClient
Let’s make things interesting and use WebClient to consume a Real World API.
In this article, we’ll consume Github’s APIs using WebClient. We’ll use WebClient to perform CRUD operations on user’s Github Repositories.
You can read on and understand the bits and pieces of WebClient from scratch or download the entire demo project with all the examples from Github.
Creating an instance of WebClient
1. Creating WebClient using the create()
method
You can create an instance of WebClient using the create()
factory method -
WebClient webClient = WebClient.create();
If you’re consuming APIs from a specific service only, then you can initialize WebClient with the baseUrl of that service like so -
WebClient webClient = WebClient.create("https://api.github.com");
2. Creating WebClient using the WebClient builder
WebClient also comes with a builder that gives you a bunch of customization options including filters, default headers, cookies, client-connectors etc -
WebClient webClient = WebClient.builder()
.baseUrl("https://api.github.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.github.v3+json")
.defaultHeader(HttpHeaders.USER_AGENT, "Spring 5 WebClient")
.build();
Making a Request using WebClient and retrieving the response
Here is how you can use WebClient to make a GET
request to Github’s List Repositories API -
public Flux<GithubRepo> listGithubRepositories(String username, String token) {
return webClient.get()
.uri("/user/repos")
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.retrieve()
.bodyToFlux(GithubRepo.class);
}
See how simple and concise the API calls are!
Assuming we have a class named GithubRepo
that confirms to the Github’s API response, the above function will return a Flux
of GithubRepo
objects.
Note that, I’m using Github’s Basic Authentication mechanism for calling the APIs. It requires your github username and a personal access token that you can generate from https://github.com/settings/tokens.
Using the exchange() method to retrieve the response
The retrieve()
method is the simplest way to get the response body. However, If you want to have more control over the response, then you can use the exchange()
method which has access to the entire ClientResponse
including all the headers and the body -
public Flux<GithubRepo> listGithubRepositories(String username, String token) {
return webClient.get()
.uri("/user/repos")
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.exchange()
.flatMapMany(clientResponse -> clientResponse.bodyToFlux(GithubRepo.class));
}
Using parameters in the request URI
You can use parameters in the request URI and pass their values separately in the uri()
function. All the parameters are surrounded by curly braces. The parameters will automatically be replaced by WebClient before making the request -
public Flux<GithubRepo> listGithubRepositories(String username, String token) {
return webClient.get()
.uri("/user/repos?sort={sortField}&direction={sortDirection}",
"updated", "desc")
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.retrieve()
.bodyToFlux(GithubRepo.class);
}
Using the URIBuilder to construct the request URI
You can also gain full programmatic control over the request URI using a UriBuilder
like so -
public Flux<GithubRepo> listGithubRepositories(String username, String token) {
return webClient.get()
.uri(uriBuilder -> uriBuilder.path("/user/repos")
.queryParam("sort", "updated")
.queryParam("direction", "desc")
.build())
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.retrieve()
.bodyToFlux(GithubRepo.class);
}
Passing Request Body in WebClient requests
If you have the request body in the form of a Mono
or a Flux
, then you can directly pass it to the body()
method in WebClient, otherwise you can just create a Mono/Flux from an object and pass it like so -
public Mono<GithubRepo> createGithubRepository(String username, String token,
RepoRequest createRepoRequest) {
return webClient.post()
.uri("/user/repos")
.body(Mono.just(createRepoRequest), RepoRequest.class)
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.retrieve()
.bodyToMono(GithubRepo.class);
}
If you have an actual value instead of a Publisher
(Flux
/Mono
), you can use the syncBody()
shortcut method to pass the request body -
public Mono<GithubRepo> createGithubRepository(String username, String token,
RepoRequest createRepoRequest) {
return webClient.post()
.uri("/user/repos")
.syncBody(createRepoRequest)
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.retrieve()
.bodyToMono(GithubRepo.class);
}
Finally, you can use various factory methods provided by BodyInserters
class to construct a BodyInserter
object and pass it in the body()
method. The BodyInserters
class contains methods to create a BodyInserter
from an Object
, Publisher
, Resource
, FormData
, MultipartData
etc -
public Mono<GithubRepo> createGithubRepository(String username, String token,
RepoRequest createRepoRequest) {
return webClient.post()
.uri("/user/repos")
.body(BodyInserters.fromObject(createRepoRequest))
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.retrieve()
.bodyToMono(GithubRepo.class);
}
Adding Filter Functions
WebClient supports request filtering using an ExchangeFilterFunction
. You can use filter functions to intercept and modify the request in any way. For example, you can use a filter function to add an Authorization
header to every request, or to log the details of every request.
The ExchangeFilterFunction
takes two arguments -
- The
ClientRequest
and - The next
ExchangeFilterFunction
in the filter chain.
It can modify the ClientRequest
and call the next ExchangeFilterFucntion
in the filter chain to proceed to the next filter or return the modified ClientRequest
directly to block the filter chain.
1. Adding Basic Authentication using a filter function
In all the examples above, we are including an Authorization
header for basic authentication with the Github API. Since this is something that is common to all the requests, you can add this logic in a filter function while creating the WebClient
.
The ExchaneFilterFunctions
API already provides a filter for basic authentication. You can use it like this -
WebClient webClient = WebClient.builder()
.baseUrl(GITHUB_API_BASE_URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, GITHUB_V3_MIME_TYPE)
.filter(ExchangeFilterFunctions
.basicAuthentication(username, token))
.build();
Now, You don’t need to add the Authorization
header in every request. The filter function will intercept every WebClient request and add this header.
2. Logging all the requests using a filter function
Let’s see an example of a custom ExchangeFilterFunction
. We’ll write a filter function to intercept and log every request -
WebClient webClient = WebClient.builder()
.baseUrl(GITHUB_API_BASE_URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, GITHUB_V3_MIME_TYPE)
.filter(ExchangeFilterFunctions
.basicAuthentication(username, token))
.filter(logRequest())
.build();
Here is the implementation of the logRequest()
filter function -
private ExchangeFilterFunction logRequest() {
return (clientRequest, next) -> {
logger.info("Request: {} {}", clientRequest.method(), clientRequest.url());
clientRequest.headers()
.forEach((name, values) -> values.forEach(value -> logger.info("{}={}", name, value)));
return next.exchange(clientRequest);
};
}
3. Using ofRequestProcessor() and ofResponseProcessor() factory methods to create filters
ExchangeFilterFunction API provides two factory methods named ofRequestProcessor()
and ofResponseProcessor()
for creating filter functions that intercepts the request and response respectively.
The logRequest()
filter function that we created in the previous section can be created using ofRequestProcessor()
factory method like this -
private ExchangeFilterFunction logRequest() {
ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
logger.info("Request: {} {}", clientRequest.method(), clientRequest.url());
clientRequest.headers()
.forEach((name, values) -> values.forEach(value -> logger.info("{}={}", name, value)));
return Mono.just(clientRequest);
});
}
If you want to intercept the WebClient response, you can use the ofResponseProcessor()
method to create a filter function like this -
private ExchangeFilterFunction logResposneStatus() {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
logger.info("Response Status {}", clientResponse.statusCode());
return Mono.just(clientResponse);
});
}
Handling WebClient Errors
The retrieve()
method in WebClient throws a WebClientResponseException
whenever a response with status code 4xx or 5xx is received.
You can customize that using the onStatus()
methods like so -
public Flux<GithubRepo> listGithubRepositories() {
return webClient.get()
.uri("/user/repos?sort={sortField}&direction={sortDirection}",
"updated", "desc")
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse ->
Mono.error(new MyCustomClientException())
)
.onStatus(HttpStatus::is5xxServerError, clientResponse ->
Mono.error(new MyCustomServerException())
)
.bodyToFlux(GithubRepo.class);
}
Note that Unlike retrieve()
method, the exchange()
method does not throw exceptions in case of 4xx or 5xx responses. You need to check the status codes yourself and handle them in the way you want to.
@ExceptionHandler
inside the controller
Handling WebClientResponseExceptions using an You can use an @ExceptionHandler
inside your controller to handle WebClientResponseException
and return an appropriate response to the clients like this -
@ExceptionHandler(WebClientResponseException.class)
public ResponseEntity<String> handleWebClientResponseException(WebClientResponseException ex) {
logger.error("Error from WebClient - Status {}, Body {}", ex.getRawStatusCode(), ex.getResponseBodyAsString(), ex);
return ResponseEntity.status(ex.getRawStatusCode()).body(ex.getResponseBodyAsString());
}
Testing Rest APIs using Spring 5 WebTestClient
WebTestClient contains request methods that are similar to WebClient. In addition, it contains methods to check the response status, header and body. You can also use assertion libraries like AssertJ
with WebTestClient.
Check out the following example for learning how to perform rest API tests using WebTestClient -
package com.example.webclientdemo;
import com.example.webclientdemo.payload.GithubRepo;
import com.example.webclientdemo.payload.RepoRequest;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class WebclientDemoApplicationTests {
@Autowired
private WebTestClient webTestClient;
@Test
@Order(1)
public void testCreateGithubRepository() {
RepoRequest repoRequest = new RepoRequest("test-webclient-repository", "Repository created for testing WebClient");
webTestClient.post().uri("/api/repos")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(Mono.just(repoRequest), RepoRequest.class)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody()
.jsonPath("$.name").isNotEmpty()
.jsonPath("$.name").isEqualTo("test-webclient-repository");
}
@Test
@Order(2)
public void testGetAllGithubRepositories() {
webTestClient.get().uri("/api/repos")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBodyList(GithubRepo.class);
}
@Test
@Order(3)
public void testGetSingleGithubRepository() {
webTestClient.get()
.uri("/api/repos/{repo}", "test-webclient-repository")
.exchange()
.expectStatus().isOk()
.expectBody()
.consumeWith(response ->
Assertions.assertThat(response.getResponseBody()).isNotNull());
}
@Test
@Order(4)
public void testEditGithubRepository() {
RepoRequest newRepoDetails = new RepoRequest("updated-webclient-repository", "Updated name and description");
webTestClient.patch()
.uri("/api/repos/{repo}", "test-webclient-repository")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(Mono.just(newRepoDetails), RepoRequest.class)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody()
.jsonPath("$.name").isEqualTo("updated-webclient-repository");
}
@Test
@Order(5)
public void testDeleteGithubRepository() {
webTestClient.delete()
.uri("/api/repos/{repo}", "updated-webclient-repository")
.exchange()
.expectStatus().isOk();
}
}
Conclusion
Congratulations! In this article, you learned how to work with Spring 5’s reactive WebClient and WebTestClient APIs.
I hope you’ll find the examples presented in this article helpful. You can download the complete sample project with all the examples from my github repository.
Thanks for reading. I really enjoyed writing this article for you and I hope to see you soon in my next article.