Goal of this example
Spring HATEOAS is the de facto standard to generate Hypermedia controls aware REST API using Spring MVC. Reading the official getting started makes the impression that domain from which json is generated has to extend HATEOAS specific ResourceSupport class, which is something I don’t like. Actually this is not the case, this example demonstrates multiple options to generate links and resources in a better way
Options to generate resources
Resuource | Inheritence | Encapsulation | |
---|---|---|---|
POJOS without HATEOAS dependency | Yes | No | Yes |
No need for “Wrapper” classes | Yes | No | No |
List of resources supported out of the box | Yes | No | No |
Options to generate links
Reference entity | Reference Controller | Build URL | |
---|---|---|---|
Automatic link generation | Yes | Yes | No |
Controllers don’t need to know about each other | Yes | No | Yes |
Compile time validation of links | No | Yes | No |
The two tables indicate that the best is to use Resources and build links by referencing entities or controllers. Interestingly 2 out of 3 techniques are not covered by the getting started guide.
Technology Used
- Spring boot 1.2.7.RELEASE
- Spring HATEOAS 1.16.0.RELEASE
A quick summary of what is HATEOAS
A well designed REST interface is built on
- Resources
- HTTP verbs
- Hypermedia controls.
- Optionally embedded resources
Hypermedia control is a fancy name for http links embedded into a resource to refer an another resource, something link this :
{ id:1, firstName:"Peter", lastName:"Test", _links:{ self:{ href:"http://localhost:8080/api/customer/1" }, invoice:{ href:"http://localhost:8080/api/invoice/customer/1" }, all-invoices:{ href:"http://localhost:8080/api/invoice" } } }
Embedded resource means a resource (properties + links) embedded into the json. By convention under the “_embedded” property. Something like this :
{ _links:{ self:{ href:"http://localhost:8080/api/customer" } }, _embedded:{ customerList:[ { id:1, firstName:"Peter", lastName:"Test", _links:{ self:{ href:"http://localhost:8080/api/customer/1" }, invoice:{ href:"http://localhost:8080/api/invoice/customer/1" }, all-invoices:{ href:"http://localhost:8080/api/invoice" } } }, { id:2, firstName:"Peter", lastName:"Test2", _links:{ self:{ href:"http://localhost:8080/api/customer/2" }, invoice:{ href:"http://localhost:8080/api/invoice/customer/2" }, all-invoices:{ href:"http://localhost:8080/api/invoice" } } } ] } }
Why is it good?
- Helps understanding the API
- UI can be data driven
The primary goal of Spring HATEOAS is to generate these links, but a prerequisite is to have resources first.
Generating resources
Options are the following
Doman object with HATEOAS dependency (Inheritance)
This is the solution described in the getting started :
public class Customer extends ResourceSupport { private int customerId; private String firstName; private String lastName; }
Generating links is easy because the domain object inherits the add() method from ResourceSupport
CustomerController.java
@RequestMapping(value = "/{id}", method = RequestMethod.GET) public Customer getCustomer(@PathVariable int id) { return addLinks(customerService.getCustomer(id)); } private static Customer addLinks(Customer customer) { Link selfLink = linkTo(methodOn(CustomerController.class).getCustomer(customer.getCustomerId())).withSelfRel(); Link invoiceLink = linkTo(methodOn(InvoiceController.class).getInvoiceByCustomerId(customer.getCustomerId())).withRel("invoice"); customer.add(selfLink); customer.add(invoiceLink); return customer; }
PROS :
- Easy to implement
CONS :
- extending ResourceSupport might not be possible if an another class is already extended
- extending ResourceSupport might not be possible if we can’t modify the domain objects
- extending ResourceSupport adds a dependency on Spring HATEOAS that might be not ideal for example when domain objects are shared among projects or layers
Create wrapper classes (Encapsulation)
This solution uses a wrapper class that extends ResourceSupport and wraps the domain object through an instance variable.
public class CustomerResource extends ResourceSupport { @JsonUnwrapped private Customer customer; public Customer getCustomer() { return customer; } public void setCustomer(Customer customer) { this.customer = customer; } }
The @JsonUnwrapped annotation will “flatten” the customer class and generate all of it’s properties on the same level as the properties of the wrapper class.
Generating the links is just a little bit more complex, we need to instantiate the wrapper class separately and set the domain class. Spring HATEOAS provides a convenience class that converts domain classes to wrapped classes.
CustomerResourceAssembler.java
public class CustomerResourceAssembler extends ResourceAssemblerSupport<Customer, CustomerResource> { public CustomerResourceAssembler() { super(CustomerController.class, CustomerResource.class); } @Override public CustomerResource toResource(Customer customer) { CustomerResource customerResource = createResourceWithId(customer.getId(), customer); Link invoiceLink = ControllerLinkBuilder.linkTo(methodOn(InvoiceController.class).getInvoiceByCustomerId(customer.getId())).withRel("invoice"); customerResource.setCustomer(customer); customerResource.add(invoiceLink); return customerResource; } }
Then the assmebler can be called by the controller
@RequestMapping(value = "{id}", method = RequestMethod.GET) public CustomerResource getCustomer(@PathVariable int id) { return customerToResource(customerService.getCustomer(id)); } private CustomerResource customerToResource(Customer customer) { return customerResourceAssembler.toResource(customer); }
PROS :
- Domain classes are independent of Spring HATEOAS
CONS :
- Need to create an extra wrapper for every domain class
Use the Resource and Resources class
Spring HATEOAS provides two classes : Resource and Resources. They are quite similar to the hand written wrapper class in the previous example, except for they use generics and contains a few more methods.
@RestController @RequestMapping(value = "/api/customer", produces = "application/hal+json") public class CustomerController { @Autowired private CustomerService customerService; @Autowired EntityLinks entityLinks; @RequestMapping(method = RequestMethod.GET) public Resources<Resource<Customer>> getCustomers() { return customerToResource(customerService.getCustomers()); } @RequestMapping(value = "/{id}", method = RequestMethod.GET) public Resource<Customer> getCustomer(@PathVariable int id) { return customerToResource(customerService.getCustomer(id)); } private Resources<Resource<Customer>> customerToResource(List<Customer> customers) { Link selfLink = linkTo(methodOn(CustomerController.class).getCustomers()).withSelfRel(); List<Resource<Customer>> customerResources = customers.stream().map(customer -> customerToResource(customer)).collect(Collectors.toList()); return new Resources<>(customerResources, selfLink); } private Resource<Customer> customerToResource(Customer customer) { Link selfLink = linkTo(methodOn(CustomerController.class).getCustomer(customer.getId())).withSelfRel(); Link allInvoiceLink = entityLinks.linkToCollectionResource(Invoice.class).withRel("all-invoice"); Link invoiceLink = linkTo(methodOn(InvoiceController.class).getInvoiceByCustomerId(customer.getId())).withRel("invoice"); return new Resource<>(customer, selfLink, invoiceLink, allInvoiceLink); } }
PROS:
- No need for extra wrapper class
- Single resources and list of resources are supported out of the box
Generating links
Spring HATEOAS provides the following options to generate links
Referencing controllers
The real power of Spring HATEOAS lies in the way links are generated. Given a controller having a method with standard @RequestMapping annotation
@RestController @RequestMapping(value = "/api/invoice", produces = "application/hal+json") public class InvoiceController { @Autowired private InvoiceService invoiceService; @RequestMapping(method = RequestMethod.GET, value = "/customer/{customerId}") public Resources<Resource<Invoice>> getInvoiceByCustomerId(@PathVariable int customerId) { Link selfLink = linkTo(methodOn(InvoiceController.class).getInvoiceByCustomerId(customerId)).withSelfRel(); return invoiceToResource(invoiceService.getInvoiceByCustomerId(customerId), selfLink); }
A link can be generated by calling it’s method with the actual parameters.
Link invoiceLink = linkTo(methodOn(InvoiceController.class).getInvoiceByCustomerId(customer.getId())).withRel("invoice");
PROS:
- Links are guaranteed to be valid due to compile time checking
CONS :
- Controllers need to know about each other
Referencing entities
A different approach is to indicate that a controller produces a domain object using the @ExposesResourceFor annotation like this :
@RestController @ExposesResourceFor(Invoice.class) @RequestMapping(value = "/api/invoice", produces = "application/hal+json") public class InvoiceController { @Autowired private InvoiceService invoiceService; @RequestMapping(method = RequestMethod.GET, value = "/customer/{customerId}") public Resources<Resource<Invoice>> getInvoiceByCustomerId(@PathVariable int customerId) { Link selfLink = linkTo(methodOn(InvoiceController.class).getInvoiceByCustomerId(customerId)).withSelfRel(); return invoiceToResource(invoiceService.getInvoiceByCustomerId(customerId), selfLink); }
Then a link can be generated by referencing the domain object’s class
Link allInvoiceLink = entityLinks.linkToCollectionResource(Invoice.class).withRel("all-invoices");
PROS:
- Controllers don’t need to cross reference each other
CONS:
- There is no compile time checking if the link is really valid
- It becomes difficult to track which method will be actually called by a link
- This kind of referencing is less flexible with parameters
Bonus
If you managed to read so far, here is your bonus : have you ever wondered what HATEOAS means? The solution is “Hypertext As The Engine Of Application State”
How to retrieve resources that are inside “embedded”?