Migrating from RestTemplate to WebClient in Spring Framework: Benefits and Implementation Guide
This article explains why RestTemplate is deprecated in Spring Framework 5+, outlines the advantages of the reactive WebClient such as non‑blocking I/O, functional style, streaming and improved error handling, and provides complete Java code examples for creating, configuring, and using WebClient synchronously and asynchronously, including timeout and error management.
In Spring Framework 5.0 and later, RestTemplate is deprecated and the newer reactive WebClient is recommended for REST calls.
WebClient offers several advantages: non‑blocking I/O built on Reactor for better scalability, a functional programming style with a fluent API, native support for streaming request/response bodies, and improved error handling and logging.
Note: even after upgrading to Spring Web 6.0.0, request timeout cannot be set in HttpRequestFactory , which is a key reason to abandon RestTemplate.
(1) Create a WebClient instance
import io.netty.channel.ChannelOption;
import io.netty.channel.ConnectTimeoutException;
import io.netty.channel.handler.timeout.ReadTimeoutException;
import io.netty.channel.handler.timeout.ReadTimeoutHandler;
import io.netty.channel.handler.timeout.TimeoutException;
import jakarta.annotation.PostConstruct;
import java.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientRequestException;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeout)
.responseTimeout(Duration.ofMillis(requestTimeout))
.doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(readTimeout)));
WebClient client = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();(2) Synchronous request (similar to RestTemplate)
public String postSynchronously(String url, String requestBody) {
LOG.info("Going to hit API - URL {} Body {}", url, requestBody);
String response = "";
try {
response = client
.method(HttpMethod.POST)
.uri(url)
.accept(MediaType.ALL)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(requestBody)
.retrieve()
.bodyToMono(String.class)
.block();
} catch (Exception ex) {
LOG.error("Error while calling API ", ex);
throw new RunTimeException("XYZ service api error: " + ex.getMessage());
} finally {
LOG.info("API Response {}", response);
}
return response;
}The block() call forces a synchronous wait; for fully reactive usage you may prefer subscribe() instead.
(3) Asynchronous request
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
public static Mono
makePostRequestAsync(String url, String postData) {
WebClient webClient = WebClient.builder().build();
return webClient.post()
.uri(url)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData("data", postData))
.retrieve()
.bodyToMono(String.class);
}Invoke the method with the target URL and URL‑encoded form data; the returned Mono will emit the response or an error.
In this example the WebClient is built with default settings; adjust the configuration as needed. Remember that block() is synchronous and may not suit all scenarios; consider using subscribe() for asynchronous handling.
(4) Handling 4xx and 5xx errors
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
public static Mono
makePostRequestAsync(String url, String postData) {
WebClient webClient = WebClient.builder()
.baseUrl(url)
.build();
return webClient.post()
.uri("/")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData("data", postData))
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse -> Mono.error(new RuntimeException("Client error")))
.onStatus(HttpStatus::is5xxServerError, clientResponse -> Mono.error(new RuntimeException("Server error")))
.bodyToMono(String.class);
}The onStatus calls inspect the response status; the first argument is a predicate that matches the status code, and the second supplies a Mono that propagates an error.
aPredicate determines whether the status matches the condition
aFunction returns a Mono containing the error information
(5) Acting on error status in subscribe()
makePostRequestAsync("https://example.com/api", "param1=value1¶m2=value2")
.subscribe(response -> {
// handle the response
System.out.println(response);
}, error -> {
// handle the error
System.err.println("An error occurred: " + error.getMessage());
if (error instanceof WebClientResponseException) {
WebClientResponseException ex = (WebClientResponseException) error;
int statusCode = ex.getStatusCode().value();
String statusText = ex.getStatusText();
System.err.println("Error status code: " + statusCode);
System.err.println("Error status text: " + statusText);
}
});The second lambda checks whether the error is a WebClientResponseException and logs the status code and text; additional actions such as retries or fallback can be added.
(6) Full example of handling success and error
responseMono.subscribe(
response -> {
// handle the response
LOG.info("SUCCESS API Response {}", response);
},
error -> {
// handle the error
LOG.error("An error occurred: {}", error.getMessage());
LOG.error("error class: {}", error.getClass());
// Server side errors
if (error instanceof WebClientResponseException) {
WebClientResponseException ex = (WebClientResponseException) error;
int statusCode = ex.getStatusCode().value();
String statusText = ex.getStatusText();
LOG.info("Error status code: {}", statusCode);
LOG.info("Error status text: {}", statusText);
if (statusCode >= 400 && statusCode < 500) {
LOG.info("Error Response body {}", ex.getResponseBodyAsString());
}
Throwable cause = ex.getCause();
LOG.error("webClientResponseException");
if (cause != null) {
LOG.info("Cause {}", cause.getClass());
if (cause instanceof ReadTimeoutException) {
LOG.error("ReadTimeout Exception");
}
if (cause instanceof TimeoutException) {
LOG.error("Timeout Exception");
}
}
}
// Client side errors such as timeouts
if (error instanceof WebClientRequestException) {
LOG.error("webClientRequestException");
WebClientRequestException ex = (WebClientRequestException) error;
Throwable cause = ex.getCause();
if (cause != null) {
LOG.info("Cause {}", cause.getClass());
if (cause instanceof ReadTimeoutException) {
LOG.error("ReadTimeout Exception");
}
if (cause instanceof ConnectTimeoutException) {
LOG.error("Connect Timeout Exception");
}
}
}
});Timeout
Per‑request timeout can be set as follows:
return webClient
.method(this.httpMethod)
.uri(this.uri)
.headers(httpHeaders -> httpHeaders.addAll(additionalHeaders))
.bodyValue(this.requestEntity)
.retrieve()
.bodyToMono(responseType)
.timeout(Duration.ofMillis(readTimeout)) // request timeout for this request
.block();Connection timeout cannot be set per request; it is a property of the WebClient and must be configured when the client is created.
Image illustrating the difference between connection timeout, read timeout, and request timeout.
Conclusion
Because RestTemplate is deprecated, developers should adopt WebClient for REST calls; its non‑blocking I/O improves performance, and it adds features such as better error handling and streaming support, while still being usable in a blocking style to mimic RestTemplate behavior.
Final note (please follow)
If this article helped you, consider liking, watching, sharing, or bookmarking it.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.