As you might have read in this blogpost, Spring is introducing a RestClient
in Spring 6.1 to interact with HTTP backends.
Now some of you might be wondering as to the why, given we already have a plethora of other options such as RestTemplate
, WebClient
, HttpUrlConnection
, …
As we can see on the javadoc page RestTemplate
got quite massive over time.
The Spring team drew lessons from this, and the reactive WebClient
was created with a fluent interface.
Now, one can certainly use this one in place of RestTemplate
, but that means dragging in extra dependencies, and well bodyToMono
looks a bit "scary" the first time you see it, doesn’t it?
So the Spring team decided to introduce the RestClient
which:
- offers a fluent API
- can be used with HTTP interfaces introduced in 6.0 (formerly only with
WebClient
) - allows us to achieve the same results as
RestTemplate
- uses a synchronous HTTP client
Now let’s have some fun with it, and please do feel free to check out the repository!
Usage
Using rest client builder
We start out by creating our rest client:
public JokeController(RestClient.Builder restClientBuilder, @Value("${jokeserviceUrl}") String jokeserviceUrl) { this.restClient = restClientBuilder.baseUrl(jokeserviceUrl).build(); }
And then we can fetch a joke using:
return this.restClient .get() .uri("/j/{jokeId}", "R7UfaahVfFd") .accept(MediaType.APPLICATION_JSON) .retrieve() .body(Joke.class);
Which as you can see is well, a lot more fluid.
When doing this call we’ll be rewarded with:
{ "id": "R7UfaahVfFd", "joke": "My dog used to chase people on a bike a lot. It got so bad I had to take his bike away.", "status": 200 }
Now in case we’re interested in the whole response including the status code, we can replace .body(Joke.class)
with .toEntity(Joke.class)
.
Or in case of a call where the response does not interest us, we can use .toBodilessEntity()
.
Using a declarative HTTP interface
Now as mentioned at the start, one of the upsides of the new RestClient is that we can also use it for declarative HTTP clients.
Let’s declare an interface to fetch a joke:
interface JokeClient { @GetExchange(url = "/j/{jokeId}", accept = "application/json") Joke getJoke(@PathVariable String jokeId); }
Then we can create our client using:
var factory = HttpServiceProxyFactory.builderFor(RestClientAdapter.create(restClient)).build(); jokeClient = factory.createClient(JokeClient.class);
After that, we can easily consume it using
@GetMapping("/joke-using-interface") Joke getJokeUsingInterface() { return this.jokeClient.getJoke("M7wPC5wPKBd"); }
And we’ll receive:
{ "id": "M7wPC5wPKBd", "joke": "Did you hear the one about the guy with the broken hearing aid? Neither did he.", "status": 200 }
Error handling
By default, RestClient will throw a subclass of RestClientException upon a 4**
or 5**
status code, but we can override this using onStatus
so that we can define our own status handlers:
.onStatus(HttpStatusCode::is4xxClientError, ((request, response) -> { throw new PunException(); }))
More granular control
In some cases, we might want to do some more advanced things for which we need access to the underlying HTTP request or HTTP response. This can be achieved by usingexchange
.
note: Status handlers are not applied when using exchange as you already have full access to the response, so you can perform any needed error handling.
return this.restClient .get() .uri("/j/{jokeId}", "UNKNOWN") .accept(MediaType.APPLICATION_JSON) .exchange((clientRequest, clientResponse) -> { // some criteria to determine our joke's too punny return "Singing in the shower is fun until you get soap in your mouth. Then it's a soap opera"; });
Wrap-up
I hope this sheds some light on the how, and why. And if you're adding Webflux
just to make use of WebClient
please consider changing to RestClient
.
In case you want to read a bit more about this: