Merikanto

一簫一劍平生意,負盡狂名十五年

Spring Boot - 07 Consuming REST Services

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 in Map<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
2
3
4
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}

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
2
3
4
public Ingredient getIngredientById(String ingredientId) {

return rest.getForObject("http://localhost:8080/ingredients/{id}", Ingredient.class, 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
2
3
4
5
6
7
public Ingredient getIngredientById(String ingredientId) {

Map<String,String> urlVariables = new HashMap<>();
urlVariables.put("id", ingredientId);

return rest.getForObject("http://localhost:8080/ingredients/{id}", Ingredient.class, urlVariables);
}

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
2
3
4
5
6
7
8
9
10
public Ingredient getIngredientById(String ingredientId) {

Map<String,String> urlVariables = new HashMap<>();
urlVariables.put("id", ingredientId);

URI url = UriComponentsBuilder.fromHttpUrl("http://localhost:8080/ingredients/{id}")
.build(urlVariables);

return rest.getForObject(url, Ingredient.class);
}

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
2
3
4
5
6
7
8
9
public Ingredient getIngredientById(String ingredientId) {

ResponseEntity<Ingredient> responseEntity =rest.getForEntity(
"http://localhost:8080/ingredients/{id}", Ingredient.class, ingredientId);

log.info("Fetched time: " + responseEntity.getHeaders().getDate());

return responseEntity.getBody();
}

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
2
3
4
5
// Choice 1: Response payload Only
public Ingredient createIngredient(Ingredient ingredient) {

return rest.postForObject("http://localhost:8080/ingredients", ingredient, Ingredient.class);
}

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
2
3
4
5
// Choice 2: Resource location Only
public URI createIngredient(Ingredient ingredient) {

return rest.postForLocation("http://localhost:8080/ingredients", ingredient);
}

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
2
3
4
5
6
7
8
9
10
// Choice 3: Response payload & Resource location
public Ingredient createIngredient(Ingredient ingredient) {

ResponseEntity<Ingredient> responseEntity = rest.postForEntity(
"http://localhost:8080/ingredients", ingredient, Ingredient.class);

log.info("New resource created at " + responseEntity.getHeaders().getLocation());

return responseEntity.getBody();
}

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
2
3
4
public void updateIngredient(Ingredient ingredient) {

rest.put("http://localhost:8080/ingredients/{id}", ingredient, ingredient.getId());
}

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
2
3
4
public void deleteIngredient(Ingredient ingredient) {

rest.delete("http://localhost:8080/ingredients/{id}", ingredient.getId());
}

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
2
3
4
5
6
ParameterizedTypeReference<Resources<Ingredient>> ingredientType = 
new ParameterizedTypeReference<Resources<Ingredient>>() {};

Resources<Ingredient> ingredientRes = traverson.follow("ingredients").toObject(ingredientType);

Collection<Ingredient> ingredients = ingredientRes.getContent();

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
2
3
4
5
6
ParameterizedTypeReference<Resources<Taco>> tacoType =
new ParameterizedTypeReference<Resources<Taco>>() {};

Resources<Taco> tacoRes = traverson.follow("tacos").follow("recents").toObject(tacoType);

Collection<Taco> tacos = tacoRes.getContent();

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
2
3
4
5
6
private Ingredient addIngredient(Ingredient ingredient) {

String ingredientsUrl = traverson.follow("ingredients").asLink().getHref();

return rest.postForObject(ingredientsUrl, ingredient, Ingredient.class);
}

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.