1. Overview
In this tutorial, we’re going to examine WebClient, which is a reactive web client introduced in Spring 5.
We’re also going to look at the WebTestClient, a WebClient designed to be used in tests.
2. What Is the WebClient?
Simply put, WebClient is an interface representing the main entry point for performing web requests.
It was created as part of the Spring Web Reactive module and will be replacing the classic RestTemplate in these scenarios. In addition, the new client is a reactive, non-blocking solution that works over the HTTP/1.1 protocol.
It’s important to note that even though it is, in fact, a non-blocking client and it belongs to the spring-webflux library, the solution offers support for both synchronous and asynchronous operations, making it suitable also for applications running on a Servlet Stack.
This can be achieved by blocking the operation to obtain the result. Of course, this practice is not suggested if we’re working on a Reactive Stack.
Finally, the interface has a single implementation, the DefaultWebClient class, which we’ll be working with.
3. Dependencies
Since we are using a Spring Boot application, all we need is the spring-boot-starter-webflux dependency to obtain Spring Framework’s Reactive Web support.
3.1. Building with Maven
Let’s add the following dependencies to the pom.xml file:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
3.2. Building with Gradle
With Gradle, we need to add the following entries to the build.gradle file:
dependencies {
compile 'org.springframework.boot:spring-boot-starter-webflux'
}
4. Working with the WebClient
In order to work properly with the client, we need to know how to:
- create an instance
- make a request
- handle the response
4.1. Creating a WebClient Instance
There are three options to choose from. The first one is creating a WebClient object with default settings:
WebClient client = WebClient.create();
The second option is to initiate a WebClient instance with a given base URI:
WebClient client = WebClient.create("http://localhost:8080");
The third option (and the most advanced one) is building a client by using the DefaultWebClientBuilder class, which allows full customization:
WebClient client = WebClient.builder()
.baseUrl("http://localhost:8080")
.defaultCookie("cookieKey", "cookieValue")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultUriVariables(Collections.singletonMap("url", "http://localhost:8080"))
.build();
4.2. Creating a WebClient Instance with Timeouts
Oftentimes, the default HTTP timeouts of 30 seconds are too slow for our needs, to customize this behavior, we can create an HttpClient instance and configure our WebClient to use it.
We can:
- set the connection timeout via the ChannelOption.CONNECT_TIMEOUT_MILLIS option
- set the read and write timeouts using a ReadTimeoutHandler and a WriteTimeoutHandler, respectively
- configure a response timeout using the responseTimeout directive
As we said, all these have to be specified in the HttpClient instance we’ll configure:
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.responseTimeout(Duration.ofMillis(5000))
.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
.addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS)));
WebClient client = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
Note that while we can call timeout on our client request as well, this is a signal timeout, not an HTTP connection, a read/write, or a response timeout; it’s a timeout for the Mono/Flux publisher.
4.3. Preparing a Request – Define the Method
First, we need to specify an HTTP method of a request by invoking method(HttpMethod method):
UriSpec<RequestBodySpec> uriSpec = client.method(HttpMethod.POST);
Or calling its shortcut methods such as get, post, and delete:
UriSpec<RequestBodySpec> uriSpec = client.post();
Note: although it might seem we reuse the request spec variables (WebClient.UriSpec, WebClient.RequestBodySpec, WebClient.RequestHeadersSpec, *WebClient.*ResponseSpec), this is just for simplicity to present different approaches. These directives shouldn’t be reused for different requests, they retrieve references, and therefore the latter operations would modify the definitions we made in previous steps.
4.4. Preparing a Request – Define the URL
The next step is to provide a URL. Once again, we have different ways of doing this.
We can pass it to the uri API as a String:
RequestBodySpec bodySpec = uriSpec.uri("/resource");
Using a UriBuilder Function:
RequestBodySpec bodySpec = uriSpec.uri(
uriBuilder -> uriBuilder.pathSegment("/resource").build());
Or as a java.net.URL instance:
RequestBodySpec bodySpec = uriSpec.uri(URI.create("/resource"));
Keep in mind that if we defined a default base URL for the WebClient, this last method would override this value.
4.5. Preparing a Request – Define the Body
Then we can set a request body, content type, length, cookies, or headers if we need to.
For example, if we want to set a request body, there are a few available ways. Probably the most common and straightforward option is using the bodyValue method:
RequestHeadersSpec<?> headersSpec = bodySpec.bodyValue("data");
Or by presenting a Publisher (and the type of elements that will be published) to the body method:
RequestHeadersSpec<?> headersSpec = bodySpec.body(
Mono.just(new Foo("name")), Foo.class);
Alternatively, we can make use of the BodyInserters utility class. For example, let’s see how we can fill in the request body using a simple object as we did with the bodyValue method*:*
RequestHeadersSpec<?> headersSpec = bodySpec.body(
BodyInserters.fromValue("data"));
Similarly, we can use the BodyInserters#fromPublisher method if we are using a Reactor instance:
RequestHeadersSpec headersSpec = bodySpec.body(
BodyInserters.fromPublisher(Mono.just("data")),
String.class);
This class also offers other intuitive functions to cover more advanced scenarios. For instance, in case we have to send multipart requests:
LinkedMultiValueMap map = new LinkedMultiValueMap();
map.add("key1", "value1");
map.add("key2", "value2");
RequestHeadersSpec<?> headersSpec = bodySpec.body(
BodyInserters.fromMultipartData(map));
All these methods create a BodyInserter instance that we can then present as the body of the request.
The BodyInserter is an interface responsible for populating a ReactiveHttpOutputMessage body with a given output message and a context used during the insertion.
A Publisher is a reactive component in charge of providing a potentially unbounded number of sequenced elements. It is an interface too, and the most popular implementations are Mono and Flux.
4.6. Preparing a Request – Define the Headers
After we set the body, we can set headers, cookies, and acceptable media types. Values will be added to those that have already been set when instantiating the client.
Also, there is additional support for the most commonly used headers like “If-None-Match”, “If-Modified-Since”, “Accept”, and “Accept-Charset”.
Here’s an example of how these values can be used:
ResponseSpec responseSpec = headersSpec.header(
HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)
.acceptCharset(StandardCharsets.UTF_8)
.ifNoneMatch("*")
.ifModifiedSince(ZonedDateTime.now())
.retrieve();
4.7. Getting a Response
The final stage is sending the request and receiving a response. We can achieve this by using either the exchangeToMono/exchangeToFlux or the retrieve method.
The exchangeToMono and exchangeToFlux methods allow access to the ClientResponse along with its status and headers:
Mono<String> response = headersSpec.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(String.class);
} else if (response.statusCode().is4xxClientError()) {
return Mono.just("Error response");
} else {
return response.createException()
.flatMap(Mono::error);
}
});
While the retrieve method is the shortest path to fetching a body directly:
Mono<String> response = headersSpec.retrieve()
.bodyToMono(String.class);
It’s important to pay attention to the *ResponseSpec.*bodyToMono method, which will throw a WebClientException if the status code is 4xx (client error) or 5xx (server error).
5. Working with the WebTestClient
The WebTestClient is the main entry point for testing WebFlux server endpoints. It has a very similar API to the WebClient, and it delegates most of the work to an internal WebClient instance focusing mainly on providing a test context. The DefaultWebTestClient class is a single interface implementation.
The client for testing can be bound to a real server or work with specific controllers or functions.
5.1. Binding to a Server
To complete end-to-end integration tests with actual requests to a running server, we can use the bindToServer method:
WebTestClient testClient = WebTestClient
.bindToServer()
.baseUrl("http://localhost:8080")
.build();
5.2. Binding to a Router
We can test a particular RouterFunction by passing it to the bindToRouterFunction method:
RouterFunction function = RouterFunctions.route(
RequestPredicates.GET("/resource"),
request -> ServerResponse.ok().build()
);
WebTestClient
.bindToRouterFunction(function)
.build().get().uri("/resource")
.exchange()
.expectStatus().isOk()
.expectBody().isEmpty();
5.3. Binding to a Web Handler
The same behavior can be achieved with the bindToWebHandler method, which takes a WebHandler instance:
WebHandler handler = exchange -> Mono.empty();
WebTestClient.bindToWebHandler(handler).build();
5.4. Binding to an Application Context
A more interesting situation occurs when we’re using the bindToApplicationContext method. It takes an ApplicationContext and analyses the context for controller beans and @EnableWebFlux configurations.
If we inject an instance of the ApplicationContext, a simple code snippet may look like this:
@Autowired
private ApplicationContext context;
WebTestClient testClient = WebTestClient.bindToApplicationContext(context)
.build();
5.5. Binding to a Controller
A shorter approach would be providing an array of controllers we want to test by the bindToController method. Assuming we’ve got a Controller class and we injected it into a needed class, we can write:
@Autowired
private Controller controller;
WebTestClient testClient = WebTestClient.bindToController(controller).build();
5.6. Making a Request
After building a WebTestClient object, all following operations in the chain are going to be similar to the WebClient until the exchange method (one way to get a response), which provides the WebTestClient.ResponseSpec interface to work with useful methods like the expectStatus, expectBody, and expectHeader:
WebTestClient
.bindToServer()
.baseUrl("http://localhost:8080")
.build()
.post()
.uri("/resource")
.exchange()
.expectStatus().isCreated()
.expectHeader().valueEquals("Content-Type", "application/json")
.expectBody().jsonPath("field").isEqualTo("value");
6. Conclusion
In this article, we explored WebClient, a new enhanced Spring mechanism for making requests on the client-side.
We also looked at the benefits it provides by going through configuring the client, preparing the request, and processing the response.
All of the code snippets mentioned in the article can be found in our GitHub repository.