This guide shows a valuable example of using Spring HATEOAS. It illustrates how to evolve your API while maintaining backward compatible. This is valuable because it reduces the need to version your API, a concept not suitable for REST services.
Before proceeding, have you read these yet?
You may wish to read them first before reading this one.
Start with a very simple example, a payroll system that tracks employees. Create a server and a client. Then, evolve the server while ensuring the original client can talk to the new one. Finally, upgrade the client and take advantage of the new features provided by the server.
Note
|
This example uses Project Lombok to reduce writing Java code. |
We all must start somewhere. So imagine you created an employee representation like this:
@Data
@NoArgsConstructor
@Entity
class Employee {
@Id @GeneratedValue
private Long id;
private String name;
private String role;
Employee(String name, String role) {
this.name = name;
this.role = role;
}
}
This domain object captures an employee’s name and role, along with a unique identifier for the data store.
-
@Data
is a Lombok annotation to turn it into a mutable value type. -
@NoArgsConstructor
creates an empty constructor, helping Jackson serialize. -
@Entity
is a JPA annotation allowing us to store it in the H2 in-memory data store used in this example.
Important
|
Why is there no @JsonIgnoreProperties(ignoreUnknown = true) annotation as shown in the basics example?
Truth be told, it’s not needed. Spring HATEOAS’s HypermediaSupportBeanDefinitionRegistrar automatically disables DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES ,
so you don’t have to include it in your POJO definition. The basics example puts it in to clarify the importance of ignoring unneeded fields.
|
With this domain object, your original server now needs a Spring Data JPA repository:
interface EmployeeRepository extends CrudRepository<Employee, Long> {
}
By extending Spring Data Common’s CrudRepository
and plugging in our domain object and it’s id’s type, we gain access to a fleet
of CRUD operations.
A key component in using Spring HATEOAS to build hypermedia is transforming objects into resources. To do that you need a resource assembler as shown in basics.
The basics example shows how to define the two most important links:
-
one for the collection (/employees)
-
one for an individual entity (/employees/1)
That’s not enough to operate. You also need to create new employees as well as navigate from the root.
Start by adding a root node for clients to start from. The idea is to find relevant collections:
@GetMapping("/")
public ResourceSupport root() {
ResourceSupport rootResource = new ResourceSupport();
rootResource.add(
linkTo(methodOn(EmployeeController.class).root()).withSelfRel(),
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"));
return rootResource;
}
Since the root node only needs to serve links, creating a bare ResourceSupport
object is quite sufficient.
-
Add a link to
EmployeeController.root
for a proper self link -
It also adds a link to
EmployeeController.findAll
and names it employees.
From here, the client can navigate (we’ll see how a little further down) to the collection of employees, as shown in basics. From there, we need to add the ability to create new employees:
@PostMapping("/employees")
public ResponseEntity<EntityModel<Employee>> newEmployee(@RequestBody Employee employee) {
Employee savedEmployee = repository.save(employee);
return ResponseEntity
.created(linkTo(methodOn(EmployeeController.class).findOne(savedEmployee.getId())).toUri())
.body(assembler.toEntityModel(savedEmployee));
}
This API can now process PUT requests, deserializing JSON found in the HTTP request body into an Employee
record.
From there, it will save it using EmployeeRepository.save
, getting back a record that includes the id.
Finally, Spring MVC’s ResponseEntity.created
factory method is used to:
-
Find the new employee’s URI and load it into the response’s Location header.
-
Convert the newly saved
Employee
into a resource using the assembler and return that in the response body.
Note
|
There’s no need to show the test data loaded up in InitDatabase.java class. Just take a peek! |
No need to show the rest of the server here. It’s vanilla Spring Boot. But one key thing, since this example runs both the server and the client on the same machine, is to run the server on a different port.
To do so, just add src/main/resources/application.yml
like this:
server:
port: 9000
This will fire the thing up on port 9000.
With our original server built, serving up employee data, it’s time to switch focus to the original client.
In this scenario, you’ll build a web app with Thymeleaf templates, but retrieves some of its data from the server app you just built.
This requires a couple extra dependencies:;
-
spring-boot-starter-thymeleaf - for Thymeleaf templating
-
json-path - you’ll see why shortly
Despite what you may think, it’s best that the client have its own version of the Employee
:
@Data
@NoArgsConstructor
class Employee {
private Long id;
private String name;
private String role;
}
There are many advantages:
-
Decouples the client from the server.
-
Clients may not want ALL the fields.
-
This client doesn’t talk to a data store, so no JPA annotations.
-
This client isn’t used to fashion test data (yet), so no need for special constructors.
All in all, it’s enough to give it the empty constructor so Jackson can handle serializing/deserializing data over the wire.
The real gold is in the HomeController
used to talk to the server:
@Controller
public class HomeController {
private static final String REMOTE_SERVICE_ROOT_URI = "http://localhost:9000";
private final RestTemplate rest;
public HomeController(RestTemplateBuilder restTemplateBuilder) {
this.rest = restTemplateBuilder.build();
}
...
}
This controller, used to construct HTML pages through Thymeleaf, needs to know the root URI of the remote service. So in this example, it is hard coded into place.
Warning
|
For fault tolerant production systems, hard coded URIs are NOT recommended. Instead, use something like Spring Cloud Netflix and it’s Eureka/Ribbon features to allow service discovery and load balanced calls. |
Parts of the controller must also perform REST calls, so we request a RestTemplateBuilder
in the constructor call, allowing Spring Boot to provide it.
Having been decorated with the HypermediaRestTemplateConfigurer
, it has all active media types applied. You are free to further customize things before invoking the build()
operation that yields a RestTemplate
.
To construct a listing of all employees, check out the following controller method:
/**
* Get a listing of ALL {@link Employee}s by querying the remote services' root URI, and then
* "hopping" to the {@literal employees} rel.
*
* NOTE: Also create a form-backed {@link Employee} object to allow creating a new entry with
* the Thymeleaf template.
*
* @param model
* @return
* @throws URISyntaxException
*/
@GetMapping
public String index(Model model) throws URISyntaxException {
Traverson client = new Traverson(new URI(REMOTE_SERVICE_ROOT_URI), MediaTypes.HAL_JSON);
CollectionModel<EntityModel<Employee>> employees = client
.follow("employees")
.toObject(new ResourcesType<EntityModel<Employee>>(){});
model.addAttribute("employee", new Employee());
model.addAttribute("employees", employees);
return "index";
}
Presuming you already understand Spring MVC, let’s focus on the RESTful bits.
-
Traverson
is used to start from the root node (REMOTE_SERVICE_ROOT_URI) and "hop" to employees. Then it fetches an object, and transforms it into Spring HATEOAS’s vendor neutralCollectionModel<EntityModel<Employee>>
structure. -
Using this, we are able to construct a
Model
object for the template.-
An employee object is created to hold an empty, form-backed bean.
-
employees is loaded up with the entire Spring HATEOAS structure, allowing the template to use what bits it wants.
-
The method then returns the name of the template to render (index
).
Note
|
Traverson is what requires having json-path on the classpath.
|
It isn’t necessary to post ALL of the Thymeleaf template index.html
, but the critical parts are here:
<table>
<thead>
<tr>
<th>Name</th><th>Role</th><th>Links</th>
</tr>
</thead>
<tbody>
<tr th:each="employee : ${employees}">
<td th:text="${employee.content.name}" />
<td th:text="${employee.content.role}" />
<td>
<ul>
<li th:each="link : ${employee.links}">
<a th:text="${link.rel}" th:href="${link.href}" />
</li>
</ul>
</td>
</tr>
</tbody>
</table>
This shows the employee data being served up inside an HTML table.
-
th:each="employee : ${employees}"
lets your iterate over each one. -
th:text="${employee.content.name}"
navigates theEntityModel<Employee>
structure (remmeber, you’re iterating over each entry ofCollectionModel<>
). -
${employee.links}
gives each entry access to a Spring HATEOASLink
. -
<a th:text="${link.rel}" th:href="${link.href}" />
lets you show the end user each link, both name and URI.
Just below the HTML table is a form for creating new employees:
<form method="post" th:action="@{/employees}" th:object="${employee}">
<input type="text" th:field="*{name}" placeholder="Name" />
<input type="text" th:field="*{role}" placeholder="Role"/>
<input type="submit" value="Submit" />
</form>
This is pure Thymeleaf. It takes the form-backed bean you just saw (th:object="${employee}"
)
and maps the HTML inputs onto its fields.
Warning
|
You could put the remote service’s employees URI, but that would subvert standard web security tactics. Instead, it’s best that all POSTs get sent back to the client’s server piece, and from there, forwarded to the remote service (just below). |
With the client put together, the last step is to forward POST /employees calls to the remote service:
/**
* Instead of putting the creation link from the remote service in the template (a security concern),
* have a local route for {@literal POST} requests. Gather up the information, and form a remote call,
* using {@link Traverson} to fetch the {@literal employees} {@link Link}.
*
* Once a new employee is created, redirect back to the root URL.
*
* @param employee
* @return
* @throws URISyntaxException
*/
@PostMapping("/employees")
public String newEmployee(@ModelAttribute Employee employee) throws URISyntaxException {
Traverson client = new Traverson(new URI(REMOTE_SERVICE_ROOT_URI), MediaTypes.HAL_JSON);
Link employeesLink = client
.follow("employees")
.asLink();
this.rest.postForEntity(employeesLink.expand().getHref(), employee, Employee.class);
return "redirect:/";
}
Again, you could hard code the path to /employees on the remote service, but that would subvert REST. Instead, you can use Traverson to open a connection to the remote service’s root URI and "hop" to employees. But instead of asking for the data, you just want the link.
Using the link, RestTemplate.postForEntity
is used to forward the data submitted in the client. Finally, a
redirect:/
is issued to Spring MVC, telling it to navigate back to the root page.
Note
|
It’s true that POST /employees on the remote service will give you back an Employee object wrapped in HAL,
but for this example, it’s not needed. Can you imagine a scenario where this information could be put to use while
redirecting the page back to home?
|
Let’s assume someone decides to update the server. This can be done in a way that doesn’t cause existing clients to break.
Looking into new-server, the updated Employee
domain object can be seen:
@Data
@NoArgsConstructor
@Entity
class Employee {
@Id @GeneratedValue
private Long id;
private String firstName;
private String lastName;
private String role;
Employee(String firstName, String lastName, String role) {
this.firstName = firstName;
this.lastName = lastName;
this.role = role;
}
...
}
The data changes to be made are shown here:
-
The single name field has been replaced with firstName and lastName.
-
The constructor call has also been adjusted to support this.
This is the part that would typically break things and force either a SOAP or CORBA update to be issued for all clients. In REST, the goal is to not break everyone, but instead provide a smoother experience
The first step is to provide a "virtual" attribute. Since the original client expects a name field, create one!
/**
* Just merge {@literal firstName} and {@literal lastName} together.
*
* @return
*/
public String getName() {
return this.firstName + " " + this.lastName;
}
This simple getter method concatenates firstName and lastName together. And Jackson will automatically turn it into a name field.
{
"firstName" : "Frodo",
"lastName" : "Baggins",
"name" : "Frodo Baggins",
"_links" : {
"self" : {
"href" : "http://localhost:9000/employees/1"
},
"employees" : {
"href" : "http://localhost:9000/employees"
}
}
}
When the client receives this document over the wire, it will deserialize it into its own Employee
domain object, throwing away
the firstName and lastName fields.
Note
|
Concerned about sending the same information twice? Don’t be. By adding just a few bytes, the cost of maintaining two versions of this API has been eliminated. If performance of a few bytes is hypercritical to the business needs at hand, then REST may not be the answer for you. |
So what happens when the original client attempts to create a new employee? You have to be able to handle that. Naturally, you must code the setter method for this virtual name field:
/**
* Split things up, and assign the first token to {@literal firstName} with everything else to {@literal lastName}.
*
* @param wholeName
*/
public void setName(String wholeName) {
String[] parts = wholeName.split(" ");
this.firstName = parts[0];
if (parts.length > 1) {
this.lastName = StringUtils.arrayToDelimitedString(Arrays.copyOfRange(parts, 1, parts.length), " ");
} else {
this.lastName = "";
}
}
This method contains the gory details of splitting up a name into parts, putting the first into firstName, and putting the rest into lastName.
From here on, the client can also evolve and take advantage of the extra fields.
Warning
|
The example code for that doesn’t depict the new-client talking to the old-server. |
This is but a simple example of making clients and services support each other through typical breaking changes.
For the next step in Spring HATEOAS, you may wish to read Spring HATEOAS - Hypermedia Example.