My objective here is not to show how to create the service itself, but to demonstrate how to write a client to the service.
The sample service that I am going to use is the "the-spring-rest-stack" written by Josh Long(@starbuxman). The specific subproject that I am going to use is the hateoas one here. If this subproject is run using "mvn jetty" command, a REST based endpoint to list the details of a user is available at http://localhost:8080/users/2 where "2" is the id of the user and gives a result of the following structure:
{ "links": [{ "rel": "self", "href": "http://localhost:8080/users/2" }, { "rel": "customers", "href": "http://localhost:8080/users/2/customers" }, { "rel": "photo", "href": "http://localhost:8080/users/2/photo" }], "id": 2, "firstName": "Lois", "profilePhotoMediaType": null, "lastName": "Lane", "username": "loislane", "password": null, "profilePhotoImported": false, "enabled": true, "signupDate": 1370201631000 }
To get to a specific customer of this user, the endpoint is at http://localhost:8080/users/2/customers/17, which gives an output of the following structure:
{ "links": [{ "rel": "self", "href": "http://localhost:8080/users/2/customers/17" }, { "rel": "user", "href": "http://localhost:8080/users/2" }], "id": 17, "signupDate": 1372461079000, "firstName": "Scott", "lastName": "Andrews", "databaseId": 17 }
Now for a consumer of these two services, the result can be represented by a java type called Resource in the Spring-hateoas project and is a generic class with the following signature:
public class Resource<T> extends ResourceSupport { protected Resource() { this.content = null; } public Resource(T content, Link... links) { this(content, Arrays.asList(links)); } ...
So the consumer of the above two services will get back the following two types:
Resource<User> user = .... //call to the service Resource<Customer> customer = ... //call to the service
The issue now is that since the "user" and "customer" above are parameterized types, if I were to bind the types using Jackson as the json processor, I would be doing something along the following lines:
ObjectMapper objectMapper = new ObjectMapper(); Resource<Customer> customer = objectMapper.readValue(customerAsJson, Resource.class); Resource<User> user = objectMapper.readValue(userAsJson, Resource.class);
The above will not work however, the reason is because of the lost type information of the parameterized Resource due to Java type erasure, Jackson wouldn't know to create an instance of Resource<User> or Resource<Customer>
The fix is to use a Super Type Token, which is essentially a way to provide the type information for libraries like Jackson and I have blogged about it before here. With this, a working code to map the json to the appropriate parameterized type would look like this:
ObjectMapper objectMapper = new ObjectMapper(); Resource<Customer> customer = objectMapper.readValue(customerAsJson, new TypeReference<Resource<Customer>>() {}); Resource<User> customer = objectMapper.readValue(userAsJson, new TypeReference<Resource<User>>() {});
Spring's client abstraction to deal with Rest based services is RestTemplate, and this can deal with a variety of message formats(xml, json, atom etc) using an abstraction called HttpMessageConverter to deal with the specifics of binding for each of the message formats.
Spring RestTemplate provides its own implementation of Super Type token to be able to bind different message formats to parameterized types, along the lines of Jackson's TypeReference, it is called the ParameterizedTypeReference.
ParameterizedTypeReference can be used to cleanly bind Rest responses for User and Customer to Java types this way:
RestTemplate restTemplate = new RestTemplate(); ResponseEntity<Resource<User>> responseEntity = restTemplate.exchange("http://localhost:8080/users/2", HttpMethod.GET, null, new ParameterizedTypeReference<Resource<User>>() {}, Collections.emptyMap()); if (responseEntity.getStatusCode() == HttpStatus.OK) { Resource<User> userResource = responseEntity.getBody(); User user = userResource.getContent(); }
RestTemplate restTemplate = new RestTemplate(); ResponseEntity<Resource<Customer>> responseEntity = restTemplate.exchange("http://localhost:8080/users/2/customers/17", HttpMethod.GET, null, new ParameterizedTypeReference<Resource<Customer>>() {}, Collections.emptyMap()); if (responseEntity.getStatusCode() == HttpStatus.OK) { Resource<Customer> customerResource = responseEntity.getBody(); Customer customer = customerResource.getContent(); }
In conclusion, ParameterizedTypeReference provides a neat way of dealing with the parameterized types and is incredibly useful in consuming the Spring Hateoas based REST services.
Thanks for the post - helped me get started.. Have you had any luck getting this to work when returning a collection? Judging by this it would be something like: new ParameterizedTypeReference>() {} - though the deserializer doesn't seem to work with this.
ReplyDeleteGreat stuff! I needed this for an @IntegrationTest in Spring Boot application! Thanks!
ReplyDeleteThere's a convenience wrapper too: https://docs.spring.io/spring-hateoas/docs/current/api/org/springframework/hateoas/mvc/TypeReferences.html
ReplyDeleteI'm INCREDIBLY surprised how lacking the Spring HATEOAS documentation is for this...
hi, this is great, I am still having some issues with a) testing collection in the main object returned and b) how to test a restresource endpoint that returns a list. My simple test is here with 2 small TODOs (its a simple spring boot app with 2 entities) https://github.com/walshe/restapp/blob/master/src/test/java/com/llibaiv/dao/CompanyRestFunctionalTest.java
ReplyDeleteIn order to retrieve a list, use Resources
DeleteI love you! Thank you so much!
Delete