Files
Greg Turnquist b228c71779 Introduce hypermedia section
Demonstrate how to:

* Create hypermedia between related aggregates
* Support multiple representations of an entity
* Support legacy routes with new resources
2017-06-06 11:24:03 -05:00

446 lines
16 KiB
Plaintext

= Spring HATEOAS - API Evolution Example
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 https://www.infoq.com/articles/roy-fielding-on-versioning[version your API], a concept not suitable for REST services.
Before proceeding, have you read these yet?
. link:../basics[Spring HATEOAS - Basic Example]
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 https://projectlombok.org[Project Lombok] to reduce writing Java code.
== Creating the Original Server
=== Defining Your Original Domain
We all must start somewhere. So imagine you created an employee representation like this:
[source,java]
----
@Data
@NoArgsConstructor
@Entity
class Employee implements Identifiable<Long> {
@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 link:../basics/src/main/java/org/springframework/hateoas/examples/Employee.java#L50[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.
=== Accessing Data
With this domain object, your original server now needs a Spring Data JPA repository:
[source,java]
----
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.
=== Converting Entities to Resources
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/README.adoc#converting-entities-to-resources,basics>>.
=== Creating a Controller
The link:../basics/src/main/java/org/springframework/hateoas/examples/EmployeeController.java[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:
[source,java]
----
@GetMapping(value = "/", produces = MediaTypes.HAL_JSON_VALUE)
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:
[source,java]
----
@PostMapping("/employees")
public ResponseEntity<Resource<Employee>> newEmployee(@RequestBody Employee employee) {
Employee savedEmployee = repository.save(employee);
return ResponseEntity
.created(linkTo(methodOn(EmployeeController.class).findOne(savedEmployee.getId())).toUri())
.body(assembler.toResource(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 link:original-server/src/main/java/org/springframework/hateoas/examples/InitDatabase.java[InitDatabase.java] class. Just take a peek!
=== Running a Server on a Different Port
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:
[source,yml]
----
server:
port: 9000
----
This will fire the thing up on port 9000.
== Creating a RESTful client
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
=== Creating the Client's Domain Object
Despite what you may think, it's best that the client have its _own_ version of the `Employee`:
[source,java]
----
@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 form links, to no need to implement the `Identifiable<T>` interface.
* 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:
[source,java]
----
@Controller
public class HomeController {
private static final String REMOTE_SERVICE_ROOT_URI = "http://localhost:9000";
private final RestTemplate rest;
public HomeController(RestTemplate restTemplate) {
this.rest = restTemplate;
}
...
}
----
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 https://spring.io/guides/gs/service-registration-and-discovery/[service discovery] and https://spring.io/guides/gs/client-side-load-balancing/[load balanced calls].
Parts of the controller must also perform REST calls, so we request a `RestTemplate` in the constructor call, allowing Spring to provide it.
To construct a listing of all employees, check out the following controller method:
[source,java]
----
/**
* 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);
Resources<Resource<Employee>> employees = client
.follow("employees")
.toObject(new ResourcesType<Resource<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 neutral `Resources<Resource<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:
[source,html]
----
<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 the `Resource<Employee>` structure (remmeber, you're iterating over each entry of `Resources<>`).
* `${employee.links}` gives each entry access to a Spring HATEOAS `Link`.
* `<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:
[source,html]
----
<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:
[source,java]
----
/**
* 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?
== Evolving the Server
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 link:new-server[new-server], the updated `Employee` domain object can be seen:
[source,java]
----
@Data
@NoArgsConstructor
@Entity
class Employee implements Identifiable<Long> {
@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!
[source,java]
----
/**
* 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.
[source,javascript]
----
{
"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:
[source,java]
----
/**
* 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 link:new-client[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 link:../hypermedia[Spring HATEOAS - Hypermedia Example].