Spring WebClient: The Next Generation Reactive HTTP Client
WebClient is a reactive HTTP client that supports non-blocking and asynchronous operations for making HTTP requests. In addition to reactive operations, it also supports synchronous and blocking requests. WebClient, which comes with Spring WebFlux, provides a synchronous/asynchronous HTTP client structure that enables us to execute all our requests.
In my previous article, I explained that we can design reactive applications with Spring Webflux. For an application to be reactive, end-to-end operations must be compatible with reactive components. This means the application needs to be fully reactive, along with all its components. When we consider HTTP requests that are made to other services (or used in our tests), it is important to ensure that they are supported by a reactive HTTP client infrastructure. You can access the project with examples for WebClient on Github in this series.
Reminder:
- Mono: It is used for Publishers that can contain 0 or 1 events.
- Flux: It is used for Publishers that can contain 0 to N events.
Why should you prefer Webclient instead of RestTemplate?
You may have heard that RestTemplate is reaching the end of its life. According to a note in the latest documentation of Spring, RestTemplate is being put into maintenance mode and it is indicated that no new features will be added.
WebClient is developed as an alternative to RestTemplate and supports synchronous, asynchronous and non-blocking operations, as well as streaming features. It provides modern and innovative methods and supports reactive programming.
From our definitions, we can quickly see that there are significant differences between the two, and it would be beneficial to explore alternatives, especially after hearing that RestTemplate will be put into maintenance mode. WebClient emerges as the strongest alternative.
Webclient vs RestTemplate
For each request, RestTemplate creates a new thread and uses it until a response is received. Once a request is sent, RestTemplate waits for the response until it reaches a previously defined timeout. This operation blocks the thread. If there are many requests in the application, it will use many threads and connections accordingly, which will result in cost and load on the server and could cause performance issues if the numbers increase.
Unlike RestTemplate, WebClient is asynchronous and non-blocking. Similar to Spring WebFlux, it enables reactive programming, and is based on an event-driven structure. When a request is made using WebClient, the thread that initiates the request continues its life without being blocked, thus providing an asynchronous structure. An important note is that WebClient also supports synchronous operations, similar to RestTemplate, through the block() method.
"Okay, but which one should I use?"
RestTemplate and WebClient are both libraries for making HTTP requests in Java. While RestTemplate has been widely used and is a good option for many projects, it creates a new thread for each request and blocks the thread until a response is received, which can result in performance issues if a large number of requests are made. On the other hand, WebClient is asynchronous and non-blocking, supporting reactive programming and event-driven architecture. It can perform similar synchronous operations as RestTemplate by using the block() method. Whether to use RestTemplate or an alternative such as WebClient would depend on the specific needs of the project. For new projects, alternatives like WebClient, Feign, OKHttp, or Retrofit 2 can be considered, but for existing projects, RestTemplate can still be used effectively.
How to use WebClient?
To use WebClient, we need to add the "spring-boot-starter-webflux" dependency to our project because it is part of Spring Web Flux.
implementation(“org.springframework.boot:spring-boot-starter-webflux:2.6.2”) // for gradle
The first and easiest way to create a WebClient instance is to use the create() factory method with default settings.
WebClient webClient = WebClient.create();
Another way to create a WebClient instance is by using the create factory method along with the URL, which allows for a more flexible approach.
WebClient webClient = WebClient.create("http://localhost:8081");
There are three different ways to create a WebClient. The first and easiest one is to create a default WebClient instance using the create() factory method with default settings.
Similarly, we can create a WebClient instance along with the URL using the create factory method.
If we only want to communicate with one service, we can create a Bean and then perform our operations without specifying the URL information.
A more flexible and user-friendly way is to create it using the WebClient.Builder. In this way, we can define all the basic settings once with the builder and then use the built instance. We can continue to develop our application without having to adjust the settings every time.
Now let's look at how it's used and create an instance via WebClient.Builder along with the general configurations.
@Beanpublic WebClient webClient() {return WebClient.builder().baseUrl("http://localhost:8081").defaultHeaders(header -> header.setBasicAuth(user, password)).defaultCookie(DEFAULT_COOKIE, COOKIE_VALUE).defaultUriVariables(Collections.singletonMap("url","gokhana.dev")).build();}
WebClient supports get(), post(), put(), patch(), delete(), options(), and head() methods.
Before preparing our requests, we need to look at a few structures.
- We can specify the path, path variables, and request parameters with uri().
- We can send headers with the request using headers().
- We can define our Cookies with cookies().
- We can use retrieve() method to perform the request and read the response or errors from the server.
- We can create a more controlled response handling structure with exchange(). We can directly convert to Flux or Mono objects using exchangeToFlux() or exchangeToMono() methods.
- We can set up a retry mechanism with retryWhen().
- Finally, we can perform the conversion of the response to Mono with bodyToMono() and to Flux with bodyToFlux().
Sending Asynchronous Requests with WebClient
After creating our WebClient bean, we can proceed to write a method for sending asynchronous requests with it.
// Controller layerMono<User> response = webClient.post().uri(uriBuilder ->uriBuilder.pathSegment("users", "{userId}").build(userId)).contentType(MediaType.APPLICATION_JSON).body(BodyInserters.fromPublisher(Mono.just(userObj))).retrieve().bodyToMono(User.class).retryWhen(Retry.fixedDelay(3, Duration.ofSeconds(3)));
Sending Synchronous Requests with WebClient
We can make a synchronous request using WebClient by specifying the returned object in the response with the .toEntity() method and then using the .block() method to make the request blocking and synchronous.
Mono<User> response = webClient.get().uri(uriBuilder ->uriBuilder.pathSegment("users", "{userId}").build(userId)).header("Authorization", "Bearer token").retrieve().toEntity(User.class).block();
The structure shown in the example provides a usable alternative to RestTemplate, so we now have a usable structure with the WebClient.
Error Handling with WebClient
Error handling is a way to dealing with errors that occur when making requests with WebClient. This can be done in a number of ways, including using the onStatus() method to handle specific HTTP status codes, using the doOnError() method to handle exceptions, or using the exchange() method to catch and handle errors in a more controlled manner. It's important to handle errors properly in order to ensure the stability and reliability of your application.
A way to handle errors and create custom exceptions is by using the .onStatus() method. With the .onStatus() method, we can handle errors that are generated either by the client or the server
Mono<User> response = webClient.post().body(BodyInserters.fromPublisher(Mono.just(userObj))).retrieve().onStatus(HttpStatus::is5xxServerError,error -> Mono.error(new RuntimeException("Error message"))).bodyToMono(User.class);
The doOnError() method is triggered when a Mono completes with an error. The onErrorResume() method can be used to intervene when any error occurs and allow the operation to continue.
public Mono<User> getUser() {return webClient().get().retrieve().bodyToMono(User.class).doOnError(error -> log.error("There is an error while sending request {}", error.getMessage())).onErrorResume(error -> Mono.just(new User()))}
Additionally, error handling can also be done in bodyToMono, although it is not the preferred usage.
Mono<String> response = webClient.get().exchangeToMono(result -> {if (!result.statusCode().is2xxSuccessful()) {return Mono.just("Error response");}return result.bodyToMono(String.class);});
We have covered synchronous and asynchronous operations, as well as error handling with WebClient. The next step in developing reactive applications should be understanding how to write tests in reactive environments, which is essential in software development.
Using WebTestClient for Tests
WebTestClient is one of the best tools for testing the rest services of our reactive applications.
It is used for testing WebFlux endpoints and behaves like WebClient. To perform a request, we need to use the exchange() method. The exchange() method includes some methods such as expectStatus, expectHeader and expectBody that can be useful for verifying the response."
@Testpublic void testUserCreate() {UserRequest request = new UserRequest(1,"gokhanadev");webTestClient.post().uri("/api/users").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).body(Mono.just(request), UserRequest.class).exchange().expectStatus().isOk().expectHeader().contentType(MediaType.APPLICATION_JSON).expectBody().jsonPath("$.name").isNotEmpty().jsonPath("$.name").isEqualTo("gokhanadev");}
Moreover, in the example above, requests created with WebTestClient are fully reactive in the same way as those made with WebClient.
Conclusion
Using WebClient in your application does not necessarily mean that your application is reactive. For an application to have reactive characteristics, it needs to have end-to-end asynchronous and non-blocking operations, and if you are using it, your database also needs to have reactive support. WebClient is one of the most important Spring WebFlux libraries that offers an alternative to RestTemplate and enables you to perform HTTP requests in a reactive manner.
You can check out my Github repo called reactive-rest-api-demo which contains examples of WebClient and WebTestClient.