I just announced the Master Class of my "REST With Spring" Course:
1. Overview
This article explains the process of creating hypermedia-driven REST web service using Spring HATEOAS project.
2. Spring-HATEOAS
The Spring HATEOAS project is a library of APIs that we can use to easily create REST representations that follow the principle of HATEOAS (Hypertext as the Engine of Application State).
Generally speaking, the principle implies that the API should guide the client through the through the application by returning relevant information about the next potential steps, along with each response.
In this article we are going to build an example using Spring HATEOAS with the goal of decoupling the client and server, and theoretically allowing the API to change its URI scheme without breaking clients.
3. Preparation
First, let’s add Spring HATEOAS dependency:
<dependency> <groupId>org.springframework.hateoas</groupId> <artifactId>spring-hateoas</artifactId> <version>0.19.0.RELEASE</version> </dependency>
Next, we have the Customer resource without Spring HATEOAS support:
public class Customer { private String customerId; private String customerName; private String companyName; // standard getters and setters }
And we have a controller class without Spring HATEOAS support:
@RestController @RequestMapping(value = "/customers") public class CustomerController { @Autowired private CustomerService customerService; @RequestMapping(value = "/{customerId}", method = RequestMethod.GET) public Customer getCustomerById(@PathVariable String customerId) { return customerService.getCustomerDetail(customerId); } }
Finally, the customer resource representation:
{ "customerId": "10A", "customerName": "Jane", "customerCompany": "ABC Company" }
4. Adding HATEOAS Support
In a Spring HATEOAS project, we don’t need to either look up the Servlet context nor concatenate the path variable to the base URI. Spring HATEOAS offers three abstractions for creating the URI – ResourceSupport, Link and ControllerLinkBuilder. These are used to create the metadata and associate it to the resource representation.
4.1. Adding Hypermedia Support to a Resource
Spring HATEOAS project provides a base class called ResourceSupport to inherit from when creating resource representation.
public class Customer extends ResourceSupport { private String customerId; private String customerName; private String companyName; // standard getters and setters }
The Customer resource extends from ResourseSupport class to inherit the add() method. So once we create a link, we can easily set that value to the resource representation without adding any new fields to it.
4.2. Creating Links
Spring HATEOAS provides a Link object to store the metadata (location or URI of the resource).
We’ll first create a simple link manually:
Link link = new Link("http://localhost:8080/spring-security-rest/api/customers/10A");
The Link object follows the Atom link syntax and consists of a rel which identifies relation to the resource and href attribute which is the actual link itself.
Here’s how the Customer resource looks now that it contains the new link:
{ "links": [{ "rel": "self", "href": "http://localhost:8080/spring-security-rest/api/customers/10A" }], "customerId": "10A", "customerName": "Jane", "customerCompany": "ABC Company" }
The URI associated with the response is qualified as a self link. The semantics of the self relation is clear – it’s simply the canonical location the Resource can be accessed at.
4.3. Creating Better Links
Another very important abstraction offered by the library is the ControllerLinkBuilder – which simplifies building URIs by avoiding hard-coded the links.
The following snippet shows building the customer self-link using the ControllerLinkBuilder class:
linkTo(CustomerController.class).slash(customer.getCustomerId()).withSelfRel();
Let’s have a look:
- the linkTo() method inspects the controller class and obtains its root mapping
- the slash() method adds the customerId value as the path variable of the link
- finally, the withSelfMethod() qualifies the relation as a self-link
5. Relations
In the previous section we’ve shown a self-referencing relation. More complex systems may involve other relations as well.
For example, a customer can have a relationship to orders. The Order class will be modeled as a resource as well:
public class Order extends ResourceSupport { private String orderId; private double price; private int quantity; // standard getters and setters }
At this point, the CustomerController controller can be extended with a method that return all orders of a particular customer:
@RequestMapping(value = "/{customerId}/orders", method = RequestMethod.GET) public List getOrdersForCustomer(@PathVariable String customerId) { return orderService.getAllOrdersForCustomer(customerId); }
An important thing to notice here, is that the hyperlink for the customer orders depends on the mapping of getOrdersForCustomer() method. We’ll refer to this types of links as method links and show how the ControllerLinkBuilder can assist in their creation.
6. Links to Controller Methods
The ControllerLinkBuilder offers rich support for Spring MVC Controllers. The following example shows how to build HATEOAS hyperlinks based on the getOrdersForCustomer() method of the CustomerController class.
List<Order> methodLinkBuilder = methodOn(CustomerController.class).getOrdersForCustomer(customer.getCustomerId()); Link ordersLink = linkTo(methodLinkBuilder).withRel("allOrders");
The methodOn() obtains the method mapping by making dummy invocation of the target method on the proxy controller, and sets the customerId as the path variable of the URI.
7. Spring HATEOAS in Action
Let’s put the self-link and method link creation all together in a getAllCustomers() method.
@RequestMapping(method = RequestMethod.GET) public List getAllCustomers() { List allCustomers = customerService.allCustomers(); for (Customer customer : allCustomers) { Link selfLink = linkTo(CustomerController.class).slash(customer.getCustomerId()).withSelfRel(); customer.add(selfLink); if (orderService.getAllOrdersForCustomer(customer.getCustomerId()).size() > 0) { List<Order> methodLinkBuilder = methodOn(CustomerController.class).getOrdersForCustomer(customer.getCustomerId()); Link ordersLink = linkTo(methodLinkBuilder).withRel("allOrders"); customer.add(ordersLink); } } return allCustomers; }
Let’s invoke the getAllCustomers() method:
curl http://localhost:8080/spring-security-rest/api/customers
And examine the result:
[{ "links": [{ "rel": "self", "href": "http://localhost:8080/spring-security-rest/api/customers/10A" }, { "rel": "allOrders", "href": "http://localhost:8080/spring-security-rest/api/customers/10A/orders" }], "customerId": "10A", "customerName": "Jane", "companyName": "ABC Company" }, { "links": [{ "rel": "self", "href": "http://localhost:8080/spring-security-rest/api/customers/20B" }, { "rel": "allOrders", "href": "http://localhost:8080/spring-security-rest/api/customers/20B/orders" }], "customerId": "20B", "customerName": "Bob", "companyName": "XYZ Company" }, { "links": [{ "rel": "self", "href": "http://localhost:8080/spring-security-rest/api/customers/30C" }], "customerId": "30C", "customerName": "Tim", "companyName": "CKV Company" }]
Within each resource representation, there is a self link and the allOrders link to extract all orders of a customer. If a customer does not have orders, then the link for orders will not appear.
This example demonstrates how Spring HATEOAS fosters API discoverability in a rest web service. If the link exists, the client can follow it and get all orders for a customer:
curl http://localhost:8080/spring-security-rest/api/customers/10A/orders
[{ "links": [{ "rel": "self", "href": "http://localhost:8080/spring-security-rest/api/customers/10A/001A" }], "orderId": "001A", "price": 150.0, "quantity": 25 }, { "links": [{ "rel": "self", "href": "http://localhost:8080/spring-security-rest/api/customers/10A/002A" }], "orderId": "002A", "price": 250.0, "quantity": 15 }]
8. Conclusion
In this tutorial, we have discussed how to build a hypermedia-driven Spring REST web service using Spring HATEOAS project.
In the example, we see that client can have a single entry point to the application and further actions can be taken based on the metadata in the response representation. This allows the server to change its URI scheme without breaking the client. Also, the application can advertise new capabilities by putting new links or URIs in the representation.
The full implementation of this article can be found in the GitHub project – this is an Eclipse based project, so it should be easy to import and run as it is.