This is the seventh post in a series of posts to cover different aspects of Spring Boot. Please note that the entire post isn’t necessarily only written in English.
In the last post, we went through how to produce REST services. In this post, we will see how to consume REST services via RestTemplate
, and how to navigate REST APIs using Traverson.
It’s not uncommon for Spring applications to both provide an API and make requests to another application’s API. This is widely used in microservices. A Spring application can consume a REST API with:
- RestTemplate: A straightforward, synchronous REST client provided by the core Spring Framework.
- Traverson: A hyperlink-aware, synchronous REST client provided by Spring HATEOAS.
- WebClient: A reactive, asynchronous REST client introduced in Spring 5.
In this post, we will focus on Rest Template and Traverson.
Normal API with Rest Template
Working with consuming REST services form the client’s perspective, we need a lot of boilerplate code to handle the low-level stuff. The client needs to:
- Create a client instance & a request object
- Execute the request & Interpret the response
- Map the response to domain objects
- Handle any exceptions that may be thrown along the way
To avoid this, Spring has RestTemplate
, which provides 41 methods in total. We will list the 12 unique operations below, each can be overloaded to equal the complete set of 41 methods:
Method | Description |
---|---|
delete |
DELETE resource data at a specified URL |
put |
PUT resource data to a specified URL |
exchange |
Executes a specified HTTP method against a URL, return ResponseEntity containing an object mapped from the response body |
execute |
Executes a specified HTTP method against a URL, returning an object mapped from the response body |
. | |
headForHeaders |
Sends an HTTP HEAD request, returning the HTTP headers for the specified resource URL |
optionsForAllow |
Sends an HTTP OPTIONS request, returning the Allow header for the specified URL |
. | |
postForLocation |
POST data to a URL, returning the URL of the newly created resource |
postForEntity |
POST data to a URL, returning a ResponseEntity containing an object mapped from the response body |
postForObject |
POST data to a URL, returning an object mapped from the response body |
getForEntity |
Sends an HTTP GET request, returning a ResponseEntity containing an object mapped from the response body |
getForObject |
Sends an HTTP GET request, returning an object mapped from a response body |
patchForObject |
Sends an HTTP PATCH request, returning the resulting object mapped from the response body |
From the list above, we can see that exchange
& execute
provide lower-level, general-purpose methods for sending HTTP requests. Most methods mentioned in the table are overloaded into 3 method forms:
- Accepts a
String
URL specification with URL parameters specified in a variable argument list - Accepts a
String
URL specification with URL parameters specified inMap<String, String>
- Accepts
java.net.URI
as the URL specification, with no support for parameterized URLs
To use RestTemplate
, we can either create a new instance:
1 | RestTemplate rest = new RestTemplate(); |
Or we can declare as a bean and inject where we need it:
1 |
|
Now we will go through the 4 HTTP methods for consuming REST services.
Get
Suppose we want to get an ingredient from the API, and if API doesn’t use HATEOAS, we can use getForObject
.:
1 | public Ingredient getIngredientById(String ingredientId) { |
Here the ingredientId
is for the /{id}
, and getForObject
accepts a String URL, and uses a variable list for URL variables. The variable parameters are assigned to the placeholders in the order that they’re given.
The second parameter Ingredeint.class
defines the type that the response should be bound to. In this case, the response data (in JSON) should be deserialized into an Ingredient
object.
Alternatively, we can use Map
to specify the URL variables:
1 | public Ingredient getIngredientById(String ingredientId) { |
In this case, the value of ingredientId
is mapped to a key of id
. When the request is made, {id}
is replaced by the map entry whose key is id
.
However, using a URI parameter requires us to construct a URI object before calling getForObject()
. Otherwise, it’s similar to both of the other variants:
1 | public Ingredient getIngredientById(String ingredientId) { |
Here the URI
object is defined from a String
specification, and its placeholders filled in from entries in a Map
, much like the previous variant of getForObject()
.
But if the client needs more than the payload body, then we need getForEntity()
. It works in much the same way as getForObject()
, but instead of returning a domain object that represents the response’s payload, it returns a ResponseEntity
object that wraps that domain object. The ResponseEntity
gives access to additional response details, such as the response headers.
Suppose that in addition to the ingredient data, we want to inspect the Date header from the response. With getForEntity()
that becomes straightforward:
1 | public Ingredient getIngredientById(String ingredientId) { |
The getForEntity()
method is overloaded with the same parameters as getForObject()
, so we can provide the URL variables as a variable list parameter, or call getForEntity()
with a URI object.
Post
RestTemplate
has 3 ways of sending a POST request.
If we wanted to receive the newly created Ingredient resource after the POST request, we use postForObject()
like this:
1 | // Choice 1: Response payload Only |
This variant of the postForObject()
method takes a String URL specification, the object to be posted to the server, and the domain type that the response body should be bound to. A fourth parameter could be a Map
of the URL variable value, or a variable list of parameters to substitute into the URL.
If the client has more need for the location of the newly created resource, then we can call postForLocation()
instead:
1 | // Choice 2: Resource location Only |
postForLocation()
works much like postForObject()
, with the exception that it returns a URI of the newly created resource instead of the resource object itself. The URI returned is derived from the response’s Location header.
If we need both the location and response payload, we can call postForEntity()
:
1 | // Choice 3: Response payload & Resource location |
Put
All three overloaded variants of put()
accept an Object that is to be serialized and sent to the given URL. As for the URL itself, it can be specified as a URI object or as a String. Just like getForObject()
and getForEntity()
, the URL variables can be provided as either a variable argument list or as a Map .
Suppose that we want to replace an ingredient resource with the data from a new Ingredient object:
1 | public void updateIngredient(Ingredient ingredient) { |
Here the URL is given as a String
, and has a placeholder that’s substituted by the given Ingredient object’s id
property. The data to be sent is the Ingredient
object itself. The put()
method returns void , so there’s nothing we need to do to handle a return value.
Delete
To completely remove a resource:
1 | public void deleteIngredient(Ingredient ingredient) { |
In this example, only the URL (specified as a String) and a URL variable value are given to delete()
. But as with the other RestTemplate
methods, the URL could be specified as a URI object or the URL parameters given as a Map .
Summary
Although the methods of RestTemplate
differ in their purpose, they’re quite similar in how they’re used.
On the other hand, if the consumed API includes hyperlinks in its response, RestTemplate
isn’t as helpful. It’s certainly possible to fetch the more detailed resource data with RestTemplate
and work with the content and links contained therein, but
it’s not trivial to do so.
Rather than struggle while consuming hypermedia APIs with RestTemplate
, we can use Traverson instead.
Hypermedia API with Traverson
Now we will consume an API by traversing the API on relation names.
We start from instantiating a Traverson object with an API’s base URI:
1 | Traverson traverson = new Traverson(URI.create("http://localhost:8080/api"), MediaTypes.HAL_JSON); |
The URL(/api
) here is the only URL we need to give to Traverson. From there we will navigate the API by link relation names.
We will also specify that the API will produce JSON responses with HAL-style hyperlinks so that Traverson knows how to parse the incoming resource data. Like RestTemplate
, we can choose to instantiate a Traverson object prior to its use, or declare it as a bean to be injected wherever it’s needed.
Now we can start consuming an API by following links. Suppose we want to retrieve a list of all ingredients. From the last post, we know that the ingredients link has an href
property that links to the ingredients resource:
1 | ParameterizedTypeReference<Resources<Ingredient>> ingredientType = |
By calling the follow()
method on the Traverson object, we can navigate to the resource whose link’s relation name is ingredients
. After we navigated to there, we need to ingest the contents of that resource by calling toObject()
.
The toObject()
method requires that we tell it what kind of object to read the data into. This can get a little tricky, since we need to read it in as a Resources<Ingredient>
object, and Java type erasure makes it difficult to provide type information for a generic type. But creating a ParameterizedTypeReference
helps with that.
As an analogy, imagine that instead of a REST API, this were a homepage on a website. And instead of REST client code, imagine that it’s you viewing that homepage in a browser. You see a link on the page that says Ingredients and you follow that link by clicking it.
Upon arriving at the next page, you read the page, which is analogous to Traverson ingesting the content as a
Resources<Ingredient>
object.
Now if we want to fetch the most recently created tacos, then starting at the home resource, we can navigate to the recent tacos resource like this:
1 | ParameterizedTypeReference<Resources<Taco>> tacoType = |
Here we follow the Tacos
link, and then follow the Recents
link. That brings you to the resource you’re interested in, so a call to toObject()
with an appropriate ParameterizedTypeReference
gets us what we want.
The .follow()
method can be simplified by listing a trail of relation names to follow:
1 | Resources<Taco> tacoRes = traverson.follow("tacos", "recents").toObject(tacoType); |
As we can see, Traverson makes it easy to navigate a HATEOAS-enabled API and consuming its resources.
But one thing it doesn’t do is offer any methods for writing to or deleting from those APIs. In contrast, RestTemplate
can write and delete resources, but doesn’t make it easy to navigate an API.
When we need to both navigate an API and update or delete resources, we’ll need to use RestTemplate
and Traverson together. Traverson can still be used to navigate to the link where a new resource will be created. Then RestTemplate
can be
given that link to do a HTTP request (GET, POST, etc).
Suppose we want to add a new Ingredient
to the Taco Cloud menu. The following addIngredient()
method uses both Traverson and RestTemplate
to post a new Ingredient to the API:
1 | private Ingredient addIngredient(Ingredient ingredient) { |
After following the Ingredients
link, we ask for the link itself by calling asLink()
. From that link, we ask for the link’s URL by calling getHref()
.
With a URL in hand, we then have everything we need to call postForObject()
on the RestTemplate
instance and save the new ingredient.