In this tutorial, we will learn how to use WebClient to consume the REST APIs, how to handle errors using WebClient, how to call REST APIs reactively using WebClient, and how to use basic authentication with WebClient.
Spring WebClient Overview
Spring's WebClient is a modern, non-blocking, and reactive client for HTTP requests. It was introduced in Spring 5 as part of the reactive stack web framework and is intended to replace the RestTemplate with a more modern, flexible, and powerful tool.
Here are some key points to understand when working with WebClient:
Reactive Programming: WebClient is part of the Spring WebFlux module and follows the reactive programming paradigm, which makes it suitable for asynchronous and non-blocking applications.
Non-Blocking IO: It is designed to support non-blocking IO operations, which means it can handle concurrent operations without thread blocking, leading to more scalable applications.
Back Pressure: It integrates with Project Reactor and supports backpressure, which allows it to handle streaming scenarios where data is produced and consumed at different rates.
Flexible Request Building: It provides a fluent API to build and execute HTTP requests. You can easily add headers, and query parameters, and set the request body.
Error Handling: WebClient provides mechanisms for handling client and server errors cleanly through status code checks and the onStatus method.
Response Processing: It supports a variety of ways to process responses, such as fetching a single object (Mono), streaming a sequence of objects (Flux), or directly into Java objects using codecs.
Header and Cookie Management: WebClient allows you to manipulate headers and cookies for each request easily. You can set these per request or globally during WebClient creation.
Authentication and Authorization: It supports various authentication mechanisms like Basic Auth, Bearer Token, and more sophisticated OAuth2 client credentials.
Client Configuration: You can customize the underlying client configuration, such as connection timeout, read/write timeout, response buffer size, and SSL details.
Filters: You can add filters to the client to manipulate the request and response or to add cross-cutting concerns like logging.
Testing Support: Spring Boot provides WebTestClient, which can be used to test WebClient interactions or your entire WebFlux application without a running server.
Interoperability: While WebClient is part of the reactive stack, it can also be used in a more traditional, synchronous way by blocking on the result. However, this should be done with caution as it negates the benefits of the reactive approach.
Global and Local Configuration: You can configure WebClient instances globally when defining the bean or on a per-request basis, providing flexibility in how different requests are handled.
Thread Model: It operates on a different threading model than servlet-based RestTemplate, utilizing fewer threads and achieving higher scalability with event-driven architecture.
Remember, while WebClient is part of the Spring WebFlux library, it can be used in any Spring Boot application, even those that don’t use the full reactive stack. This makes WebClient a versatile tool for RESTful interactions in modern Spring applications.
Pre-requisites
Add Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
}
Create a WebClient Instance
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient(WebClient.Builder builder) {
return builder.baseUrl("http://localhost:8080").build();
}
}
Using WebClient as REST Client
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Flux;
@Service
public class UserServiceClient {
private final WebClient webClient;
@Autowired
public UserServiceClient(WebClient webClient) {
this.webClient = webClient;
}
// Create a new User
public Mono<User> createUser(User user) {
return webClient.post()
.bodyValue(user)
.retrieve()
.bodyToMono(User.class);
}
// Get a User by ID
public Mono<User> getUserById(Long userId) {
return webClient.get()
.uri("/{id}", userId)
.retrieve()
.bodyToMono(User.class);
}
// Get all Users
public Flux<User> getAllUsers() {
return webClient.get()
.retrieve()
.bodyToFlux(User.class);
}
// Update a User
public Mono<User> updateUser(Long userId, User user) {
return webClient.put()
.uri("/{id}", userId)
.bodyValue(user)
.retrieve()
.bodyToMono(User.class);
}
// Delete a User
public Mono<String> deleteUser(Long userId) {
return webClient.delete()
.uri("/{id}", userId)
.retrieve()
.bodyToMono(String.class);
}
}
In this service: private final WebClient webClient;
@Autowired
public UserServiceClient(WebClient webClient) {
this.webClient = webClient;
}
getUserById(Long userId): This method sends a GET request to retrieve a user by their ID.
getAllUsers(): This method sends a GET request to fetch all users.
updateUser(Long userId, User user): This method sends a PUT request to update an existing user.
deleteUser(Long userId): This method sends a DELETE request to remove a user by ID.
Error Handling using WebClient
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Flux;
@Service
public class UserServiceClient {
private final WebClient webClient;
@Autowired
public UserServiceClient(WebClient webClient) {
this.webClient = webClient;
}
// Create a new User
public Mono<User> createUser(User user) {
return webClient.post()
.bodyValue(user)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse ->
Mono.error(new RuntimeException("Client Error")))
.onStatus(HttpStatus::is5xxServerError, clientResponse ->
Mono.error(new RuntimeException("Server Error")))
.bodyToMono(User.class)
.onErrorResume(WebClientResponseException.class, e ->
Mono.error(new RuntimeException("Error: " + e.getResponseBodyAsString())));
}
// Get a User by ID
public Mono<User> getUserById(Long userId) {
return webClient.get()
.uri("/{id}", userId)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse ->
Mono.error(new RuntimeException("Not Found")))
.onStatus(HttpStatus::is5xxServerError, clientResponse ->
Mono.error(new RuntimeException("Server Error")))
.bodyToMono(User.class)
.onErrorResume(WebClientResponseException.class, e ->
Mono.error(new RuntimeException("Error: " + e.getResponseBodyAsString())));
}
// Get all Users
public Flux<User> getAllUsers() {
return webClient.get()
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse ->
Mono.error(new RuntimeException("Client Error")))
.onStatus(HttpStatus::is5xxServerError, clientResponse ->
Mono.error(new RuntimeException("Server Error")))
.bodyToFlux(User.class)
.onErrorResume(WebClientResponseException.class, e ->
Mono.error(new RuntimeException("Error: " + e.getResponseBodyAsString())));
}
// Update a User
public Mono<User> updateUser(Long userId, User user) {
return webClient.put()
.uri("/{id}", userId)
.bodyValue(user)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse ->
Mono.error(new RuntimeException("Not Found")))
.onStatus(HttpStatus::is5xxServerError, clientResponse ->
Mono.error(new RuntimeException("Server Error")))
.bodyToMono(User.class)
.onErrorResume(WebClientResponseException.class, e ->
Mono.error(new RuntimeException("Error: " + e.getResponseBodyAsString())));
}
// Delete a User
public Mono<String> deleteUser(Long userId) {
return webClient.delete()
.uri("/{id}", userId)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse ->
Mono.error(new RuntimeException("Not Found")))
.onStatus(HttpStatus::is5xxServerError, clientResponse ->
Mono.error(new RuntimeException("Server Error")))
.bodyToMono(String.class)
.onErrorResume(WebClientResponseException.class, e ->
Mono.error(new RuntimeException("Error: " + e.getResponseBodyAsString())));
}
}
In the updated UserServiceClient, we have added proper error handling for different scenarios:
Reactively Consuming the API using WebClient
public void useClientService() {
// Create User
createUser(new User("Jane", "Doe"))
.subscribe(
user -> System.out.println("Created user: " + user),
error -> System.err.println("Error during user creation: " + error.getMessage())
);
// Fetch User
getUserById(1L)
.subscribe(
user -> System.out.println("Fetched user: " + user),
error -> System.err.println("Error during fetch: " + error.getMessage())
);
// Update User
updateUser(1L, new User("Jane", "Doe Updated"))
.subscribe(
user -> System.out.println("Updated user: " + user),
error -> System.err.println("Error during update: " + error.getMessage())
);
// Delete User
deleteUser(1L)
.subscribe(
message -> System.out.println("User deleted."),
error -> System.err.println("Error during deletion: " + error.getMessage())
);
}
Here, subscribe is called with two lambda expressions: one for the success case and one for the error case. This allows for a simple way to handle the asynchronous results of these operations.
WebClient with Basic Authentication
Global Basic Authentication Configuration
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.http.HttpHeaders;
import org.springframework.util.Base64Utils;
@Configuration
public class WebClientConfig {
private static String encodeCredentials(String username, String password) {
String credentials = username + ":" + password;
return "Basic " + Base64Utils.encodeToString(credentials.getBytes());
}
@Bean
public WebClient webClient() {
String encodedCredentials = encodeCredentials("user", "password");
return WebClient.builder()
.baseUrl("http://localhost:8080/api/users")
.defaultHeader(HttpHeaders.AUTHORIZATION, encodedCredentials)
.build();
}
}
This will add the Authorization header with basic authentication to every request made by this WebClient. Per-Request Basic Authentication Configuration
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.http.HttpHeaders;
import org.springframework.util.Base64Utils;
public class UserServiceClient {
private final WebClient webClient;
public UserServiceClient(WebClient webClient) {
this.webClient = webClient;
}
private String basicAuthHeader(String username, String password) {
String auth = username + ":" + password;
return "Basic " + Base64Utils.encodeToString(auth.getBytes());
}
public Mono<User> getUserById(Long userId, String username, String password) {
return webClient.get()
.uri("/{id}", userId)
.header(HttpHeaders.AUTHORIZATION, basicAuthHeader(username, password))
.retrieve()
.bodyToMono(User.class);
}
// Other methods...
}
Comments
Post a Comment
Leave Comment