Webclient
When to Use WebClient
When building reactive applications (e.g., using Spring WebFlux).
When you need non-blocking I/O for better scalability and performance.
When working with streaming APIs or handling large datasets.
When you want to use functional programming for HTTP requests.
Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
Sample Usages
// get example
public <T> Mono<T> get(String url, Class<T> responseType,String token) {
return webClient.get()
.uri(getUriBuilderURIFunction(url))
.header(MessageConst.AUTHORIZATION, MessageConst.BEARER +token)
.retrieve()
.bodyToMono(responseType)
.timeout(timeout)
.doOnSuccess(response -> log.info("Received successful response from GET {}", url))
.doOnError(error -> log.error("Error in GET request to {}: {}", url, error.getMessage()))
.retryWhen(
Retry.backoff(retry, Duration.ofSeconds(1)).filter(e->!e.getMessage().equals("401"))
.doBeforeRetry(retrySignal -> log.warn("Retrying GET request to {}", url))
)
.onErrorResume(this::handleError);
}
// post example
public <T> Mono<T> post(String url, Object body, Class<T> responseType,String token) {
return webClient.post().uri(getUriBuilderURIFunction(url))
.header(MessageConst.AUTHORIZATION, MessageConst.BEARER +token)
.bodyValue(body)
.retrieve()
.bodyToMono(responseType)
.timeout(timeout)
.doOnSuccess(response -> log.info("Received successful response from POST {}", url))
.doOnError(error -> log.error("Error in POST request to {}: {}", url, error.getMessage()))
.onErrorResume(this::handleError);
}
Exception handling
We can use reactive extreame to do the handling . We have certain methods like doOnError, doOnStatus, onErrorResume etc, using them we can define how we want to handle it.
// in web client
.onStatus(
HttpStatus::is4xxClientError,
clientResponse -> Mono.error(new RuntimeException("Client error occurred"))
)
.onStatus(
HttpStatus::is5xxServerError,
clientResponse -> Mono.error(new RuntimeException("Server error occurred"))
)
// or
.onErrorResume(this::handleError)
// custom handler method
private <T> Mono<T> handleError(Throwable error) {
if (error instanceof WebClientResponseException wcre) {
HttpStatusCode status = wcre.getStatusCode();
String body = wcre.getResponseBodyAsString();
if (status.is4xxClientError()) {
log.error("Client error: {} {}", status, body);
return Mono.error(new ClientException("Client error: " + status + ", body: " + body));
} else if (status.is5xxServerError()) {
log.error("Server error: {} {}", status, body);
return Mono.error(new ServerException("Server error: " + status + ", body: " + body));
}
} else if (error instanceof java.util.concurrent.TimeoutException) {
log.error("Request timed out");
return Mono.error(new TimeoutException("Request timed out after " + timeout.getSeconds() + " seconds"));
} else if(Exceptions.isRetryExhausted(error)){
log.error("Retry exhausted ");
return Mono.error(new TimeoutException("Retry exhausted after " + retry + " attempts"));
}
log.error("Unexpected error", error);
return Mono.error(new GenericApiException("An unexpected error occurred", error));
}
Log request and response of requests
To log request and reponse in webclient we can utilize the filters.
We can use ExchangeFilterFunction that have methods like ofRequestProcessor and ofResponseProcessor .
Create log filters for request and response and attach in configuration.
private WebClient getBuilder(String applicationProperties) {
return WebClient.builder()
.baseUrl(applicationProperties)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.filter(logRequest())
.filter(logResponse())
.build();
}
private ExchangeFilterFunction logRequest() {
return ExchangeFilterFunction.ofRequestProcessor(request -> {
log.info("Request: {} {}", request.method(), request.url());
return Mono.just(request);
});
}
private ExchangeFilterFunction logResponse() {
return ExchangeFilterFunction.ofResponseProcessor(response -> {
log.info("Response: {}", response.statusCode());
return Mono.just(response);
});
}
Multiple web clients bean
Yes we can have multiple webclients bean present in the system. We need to create multiple beans with different bean names , then whenever required can use @Qualifier to inject the bean required.
// in configuratrion
@Bean
public WebClient userWebClient(){
return getBuilder(applicationProperties.getUserServiceUrl());
}
@Bean
public WebClient authWebClient(){
return getBuilder(applicationProperties.getAuthServiceUrl());
}
@Bean
public WebClient vendorWebClient(){
return getBuilder(applicationProperties.getVendorServiceUrl());
}
// usages
@Qualifier("vendorWebClient") WebClient webClient
Custom Headers and Query Parameters
Add custom headers and query parameters dynamically.
public Mono<String> fetchDataWithCustomHeadersAndParams(String paramValue) {
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/data")
.queryParam("param", paramValue)
.build())
.header("Custom-Header", "value")
.retrieve()
.bodyToMono(String.class);
}
Parallel requests
We can utilize zip method from reactive streams for this .
public Mono<String> fetchParallelData() {
Mono<String> data1 = webClient.get()
.uri("/data1")
.retrieve()
.bodyToMono(String.class);
Mono<String> data2 = webClient.get()
.uri("/data2")
.retrieve()
.bodyToMono(String.class);
return Mono.zip(data1, data2, (d1, d2) -> d1 + " " + d2);
}
Streaming Data
Can be used to stream data from an API (e.g., Server-Sent Events or large JSON arrays).
public Flux<String> streamData() {
return webClient.get()
.uri("/stream")
.retrieve()
.bodyToFlux(String.class); // Stream data as Flux
}
Caching
Implement caching for responses.
public Mono<String> fetchDataWithCache() {
return webClient.get()
.uri("/data")
.retrieve()
.bodyToMono(String.class)
.cache(Duration.ofMinutes(10)); // Cache the response for 10 minutes
}
Reactive Caching with Redis
Use Redis for reactive caching of responses.
public Mono<String> fetchDataWithRedisCache() {
return webClient.get()
.uri("/data")
.retrieve()
.bodyToMono(String.class)
.cache(Duration.ofMinutes(10))
.flatMap(data -> redisTemplate.opsForValue().set("cacheKey", data).thenReturn(data));
}
File Upload
Upload a file using WebClient
with multipart/form-data
.
public Mono<String> uploadFile(File file) {
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("file", new FileSystemResource(file));
return webClient.post()
.uri("/upload")
.body(BodyInserters.fromMultipartData(builder.build()))
.retrieve()
.bodyToMono(String.class);
}
File Download
Download a file and save it to the local filesystem.
public Mono<Void> downloadFile(String url, Path savePath) {
return webClient.get()
.uri(url)
.retrieve()
.bodyToMono(Resource.class)
.flatMap(resource -> {
try {
Files.copy(resource.getInputStream(), savePath, StandardCopyOption.REPLACE_EXISTING);
return Mono.empty();
} catch (IOException e) {
return Mono.error(e);
}
});
}
Custom Serialization/Deserialisation
Use custom serialization/deserialisation for requests and responses.
public Mono<MyData> fetchDataWithCustomDeserialization() {
return webClient.get()
.uri("/data")
.retrieve()
.bodyToMono(new ParameterizedTypeReference<MyData>() {});
}
Custom SSL Configuration
Configure custom SSL settings .
public WebClient createWebClientWithCustomSSL() {
SslContext sslContext = SslContextBuilder.forClient().build();
HttpClient httpClient = HttpClient.create().secure(sslContextSpec -> sslContextSpec.sslContext(sslContext));
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
Custom Metrics
Add custom metrics for monitoring WebClient
requests.
public Mono<String> fetchDataWithMetrics() {
return webClient.get()
.uri("/data")
.retrieve()
.bodyToMono(String.class)
.metrics() // Add metrics
.name("webclient.requests")
.tag("uri", "/data")
.register(registry); // attach to registery
}
Last updated