553 lines
19 KiB
Plaintext
553 lines
19 KiB
Plaintext
= Spring HATEOAS - Affordances Example
|
|
|
|
This guide shows the opportunities to build hypermedia that http://amundsen.com/blog/archives/1109[affords] operations using https://rwcbook.github.io/hal-forms/[HAL-FORMS].
|
|
|
|
Before proceeding, have you read these yet?
|
|
|
|
. link:../basics[Spring HATEOAS - Basic Example]
|
|
. link:../api-evolution[Spring HATEOAS - API Evolution Example]
|
|
. link:../hypermedia[Spring HATEOAS - Hypermedia Example]
|
|
|
|
You may wish to read them first before reading this one.
|
|
|
|
NOTE: This example uses https://projectlombok.org[Project Lombok] to reduce writing Java code.
|
|
|
|
== Building "affordances"
|
|
|
|
For starters, what is an *affordance*? It's what the hypermedia lets you _do_. With a HAL document, you are provided with data and links, but nothing else.
|
|
|
|
This is the beauty of REST. By not having one megaspec, REST is able to adjust and adapt by adopting new mediatypes. So far, we've seen HAL, a lightweight
|
|
mediatype that inludes data and links. However, HAL doesn't illustrate what can be done with links. It's possible to use content negotation against
|
|
the links to see what REST verbs are supported, but even with that discovery, we still wouldn't know all the characteristics of a resource's properties.
|
|
|
|
HAL-FORMS, an extension of HAL, attempts to bridge this gap. It supports the HAL concept of data + links, but introduces another element, *$$_templates$$*.
|
|
*$$_templates$$* make it possible to show all the operations possible as the attributes needed for each operation.
|
|
|
|
== Defining Your Domain
|
|
|
|
This example takes off where Basics and API Evolution end: an employee payroll system. Only this time, you will create hypermedia-driven operations in the form of *templates*.
|
|
|
|
For starters, here is the basic definition:
|
|
|
|
[source,java]
|
|
----
|
|
Data
|
|
@Entity
|
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
|
@AllArgsConstructor
|
|
class Employee implements Identifiable<Long> {
|
|
|
|
@Id @GeneratedValue
|
|
private Long id;
|
|
private String firstName;
|
|
private String lastName;
|
|
private String role;
|
|
|
|
/**
|
|
* Useful constructor when id is not yet known.
|
|
*/
|
|
Employee(String firstName, String lastName, String role) {
|
|
|
|
this.firstName = firstName;
|
|
this.lastName = lastName;
|
|
this.role = role;
|
|
}
|
|
|
|
public Optional<Long> getId() {
|
|
return Optional.ofNullable(this.id);
|
|
}
|
|
}
|
|
----
|
|
|
|
This domain object should look quite familiar by now.
|
|
|
|
A corresponding Spring Data JPA repository is defined:
|
|
|
|
[source,java]
|
|
----
|
|
interface EmployeeRepository extends CrudRepository<Employee, Long> {
|
|
}
|
|
----
|
|
|
|
From here, we need to build a resource assembler.
|
|
|
|
First of all, we need to extend `SimpleIdentifiableResourceAssembler` and hook it to `Employee`:
|
|
|
|
[source,java]
|
|
----
|
|
@Component
|
|
class EmployeeResourceAssembler extends SimpleIdentifiableResourceAssembler<Employee> {
|
|
|
|
/**
|
|
* Link the {@link Employee} domain type to the {@link EmployeeController} using this
|
|
* {@link SimpleIdentifiableResourceAssembler} in order to generate both {@link org.springframework.hateoas.Resource}
|
|
* and {@link org.springframework.hateoas.Resources}.
|
|
*/
|
|
EmployeeResourceAssembler() {
|
|
super(EmployeeController.class);
|
|
}
|
|
|
|
...
|
|
}
|
|
----
|
|
|
|
This links the domain type of `Employee` to the Spring MVC controller (you'll build further down) `EmployeeController`. This makes it possible to
|
|
build links.
|
|
|
|
Next you need to define the links for a single resource `Employee` (denoted by Spring HATEOAS's `Resource<Employee>`):
|
|
|
|
[source,java]
|
|
----
|
|
@Component
|
|
class EmployeeResourceAssembler extends SimpleIdentifiableResourceAssembler<Employee> {
|
|
|
|
...
|
|
|
|
/**
|
|
* Define links to add to every {@link Resource}.
|
|
*
|
|
* @param resource
|
|
*/
|
|
@Override
|
|
protected void addLinks(Resource<Employee> resource) {
|
|
|
|
resource.getContent().getId()
|
|
.ifPresent(id -> {
|
|
resource.add(getCollectionLinkBuilder().slash(resource.getContent()).withSelfRel()
|
|
.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, id))));
|
|
});
|
|
|
|
resource.add(getCollectionLinkBuilder().withRel(this.getRelProvider().getCollectionResourceRelFor(this.getResourceType())));
|
|
}
|
|
...
|
|
}
|
|
----
|
|
|
|
A link is built by looking up the `getCollectionLinkBuilder()` to find the collection name (using Spring HATEOAS's `RelProvider` to turn `Employee` into `employees`),
|
|
followed by a slash ("/"), and the content of the `Resource<Employee>`. Because this resource implements `Identifiable`, Spring
|
|
HATEOAS know how to get the `id` of the resource. This URI is converted into a Spring HATEOAS *self* `Link` through `withSelfRel()`.
|
|
|
|
When it comes to HAL documents, this is all we need. HAL is based on data combined with simple links.
|
|
|
|
Affordances is about chaining related links together to support richer mediatypes. In this case, we have HAL-FORMS support. This means
|
|
we can connect the *GET* link to its related *PUT* link using the `andAffordance(afford(methodOn(...))`.
|
|
|
|
The `methodOn()` API works just like the other examples show. But the `afford()` operation, based on web-specific technology (in this
|
|
case Spring MVC), is able to look up details about the endpoint and flesh out the *_templates* section of a HAL-FORMS document.
|
|
|
|
Similarly, you need to link the aggregate *GET* link to the corresponding *POST* link. See below:
|
|
|
|
[source,java]
|
|
----
|
|
@Component
|
|
class EmployeeResourceAssembler extends SimpleIdentifiableResourceAssembler<Employee> {
|
|
|
|
...
|
|
/**
|
|
* Define links to add to {@link Resources} collection.
|
|
*
|
|
* @param resources
|
|
*/
|
|
@Override
|
|
protected void addLinks(Resources<Resource<Employee>> resources) {
|
|
resources.add(getCollectionLinkBuilder().withSelfRel()
|
|
.andAffordance(afford(methodOn(EmployeeController.class).newEmployee(null))));
|
|
}
|
|
}
|
|
----
|
|
|
|
This code uses the same `getCollectionBuilder()` to point to the collection (`employees`) and connect to the controller's `newEmployee`
|
|
Spring MVC method.
|
|
|
|
So you want to round this out by defining the controller.
|
|
|
|
[source,java]
|
|
----
|
|
@RestController
|
|
class EmployeeController {
|
|
|
|
private final EmployeeRepository repository;
|
|
private final EmployeeResourceAssembler assembler;
|
|
|
|
EmployeeController(EmployeeRepository repository, EmployeeResourceAssembler assembler) {
|
|
|
|
this.repository = repository;
|
|
this.assembler = assembler;
|
|
}
|
|
|
|
...
|
|
}
|
|
----
|
|
|
|
For starters, you can declare a controller like this:
|
|
|
|
* `@RestController` makes the entire controller render responses as direct JSON and not rendered templates.
|
|
* Injects `EmployeeRepository` and `EmployeeResourceAssembler` through constructor injection.
|
|
|
|
Next, you need to define endpoints for the aggregate collection:
|
|
|
|
[source,java]
|
|
----
|
|
@RestController
|
|
class EmployeeController {
|
|
|
|
...
|
|
|
|
@GetMapping("/employees")
|
|
ResponseEntity<Resources<Resource<Employee>>> findAll() {
|
|
return ResponseEntity.ok(
|
|
assembler.toResources(repository.findAll()));
|
|
}
|
|
|
|
@PostMapping("/employees")
|
|
ResponseEntity<?> newEmployee(@RequestBody Employee employee) {
|
|
|
|
return repository.save(employee).getId()
|
|
.map(this::findOne)
|
|
.map(HttpEntity::getBody)
|
|
.flatMap(ResourceSupport::getId)
|
|
.map(Link::getHref)
|
|
.map(href -> {
|
|
try {
|
|
return new URI(href);
|
|
} catch (URISyntaxException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
})
|
|
.map(uri -> ResponseEntity.noContent().location(uri).build())
|
|
.orElse(ResponseEntity.badRequest().body("Unable to create " + employee));
|
|
}
|
|
|
|
...
|
|
}
|
|
----
|
|
|
|
This fragment of the controller shows:
|
|
|
|
* A *GET* call for the aggregate collection is defined. It uses the repository's `findAll()` method and transforms it into a `Resources<Resource<Employee>>`
|
|
using the `EmployeeResourceAssembler`.
|
|
* A *POST* call for creating new employees is also defined, on the same URI. `@RequestBody` tells Spring MVC to deserialize the request body into an `Employee` object,
|
|
which is then sent through the repository's `save()` operation. From there, you grab the `Optional` *id*
|
|
|
|
The premise is that the *POST* endpoint is related to the *GET* endpoint. In other words, the URI at `/employees` services a *GET* call while _also affording_ a *POST* call.
|
|
|
|
To get this operational, you must do one additional step--reconfigure hypermedia. By default, Spring Boot sets things up for HAL. To switch to HAL-FORMS, you need to create this:
|
|
|
|
[source,java]
|
|
----
|
|
@Configuration
|
|
@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS)
|
|
public class HypermediaConfiguration {
|
|
|
|
@Bean
|
|
public static HalObjectMapperConfigurer halObjectMapperConfigurer() {
|
|
return new HalObjectMapperConfigurer();
|
|
}
|
|
|
|
private static class HalObjectMapperConfigurer
|
|
implements BeanPostProcessor, BeanFactoryAware {
|
|
|
|
private BeanFactory beanFactory;
|
|
|
|
/**
|
|
* Assume any {@link ObjectMapper} starts with {@literal _hal} and ends with {@literal Mapper}.
|
|
*/
|
|
@Override
|
|
public Object postProcessBeforeInitialization(Object bean, String beanName)
|
|
throws BeansException {
|
|
if (bean instanceof ObjectMapper && beanName.startsWith("_hal") && beanName.endsWith("Mapper")) {
|
|
postProcessHalObjectMapper((ObjectMapper) bean);
|
|
}
|
|
return bean;
|
|
}
|
|
|
|
private void postProcessHalObjectMapper(ObjectMapper objectMapper) {
|
|
try {
|
|
Jackson2ObjectMapperBuilder builder = this.beanFactory.getBean(Jackson2ObjectMapperBuilder.class);
|
|
builder.configure(objectMapper);
|
|
} catch (NoSuchBeanDefinitionException ex) {
|
|
// No Jackson configuration required
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Object postProcessAfterInitialization(Object bean, String beanName)
|
|
throws BeansException {
|
|
return bean;
|
|
}
|
|
|
|
@Override
|
|
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
|
this.beanFactory = beanFactory;
|
|
}
|
|
}
|
|
}
|
|
----
|
|
|
|
There is lot packed in here:
|
|
|
|
* `@Configuration` makes this class automatically picked up by Spring Boot's component scanning.
|
|
* `@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS)` activates Spring HATEOAS's hypermedia support, setting the format to HAL-FORMS.
|
|
* When you use this annotation, all of Spring Boot's autoconfigured hypermedia support is disabled. You are taking over, so the rest of the code is
|
|
about finding any registered `ObjectMapper` beans in the app context and registering the HAL-FORMS support through builtin callbacks.
|
|
|
|
WARNING: You currently cannot support more than one hypermedia-based mediatype as this point in time. If you try to use both `HAL` and `HAL_FORMS` in the annotation,
|
|
Spring Boot will fail to launch.
|
|
|
|
IMPORTANT: We are working on simplifying the means to select different *and* multiple hypermedia formats.
|
|
|
|
Before launching the application, you'll want to pre-load some test data:
|
|
|
|
[source,java]
|
|
----
|
|
@Component
|
|
class DatabaseLoader {
|
|
|
|
/**
|
|
* Use Spring to inject a {@link EmployeeRepository} that can then load data. Since this will run
|
|
* only after the app is operational, the database will be up.
|
|
*
|
|
* @param repository
|
|
*/
|
|
@Bean
|
|
CommandLineRunner init(EmployeeRepository repository) {
|
|
return args -> {
|
|
repository.save(new Employee("Frodo", "Baggins", "ring bearer"));
|
|
repository.save(new Employee("Bilbo", "Baggins", "burglar"));
|
|
};
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
This little database loader will:
|
|
|
|
* Be picked up by component scanning due to the `@Component` annotation.
|
|
* The `CommandLineRunner` bean is executed by Spring Boot after the entire application context is up.
|
|
* Inside that chunk of code, the injected `EmployeeRepository` is used to create a couple database entries.
|
|
|
|
NOTE: The database for this example is `H2`, an in-memory database that always starts up empty. If you switch to a persistent store, you probably need
|
|
to include the extra step to delete old data or you'll get multiple entries.
|
|
|
|
If you launch the application and `GET /employees`, you can expect the following HAL-FORMS result:
|
|
|
|
[source,javascript]
|
|
----
|
|
{
|
|
"_embedded": {
|
|
"employees": [...]
|
|
},
|
|
"_links": {
|
|
"self": {
|
|
"href": "http://localhost:8080/employees"
|
|
}
|
|
},
|
|
"_templates": {
|
|
"default": {
|
|
"title": null,
|
|
"method": "post",
|
|
"contentType": "",
|
|
"properties":[
|
|
{
|
|
"name": "firstName",
|
|
"required": true
|
|
},
|
|
{
|
|
"name": "id",
|
|
"required": true
|
|
},
|
|
{
|
|
"name": "lastName",
|
|
"required": true
|
|
},
|
|
{
|
|
"name": "role",
|
|
"required": true
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
----
|
|
|
|
This fragment of JSON can be described as follows:
|
|
|
|
* The *_embedded* chunk has been shrunk down for space reasons. It contains an array of `Employee` resources, which you'll see in more detail further down.
|
|
* The *_links* section is just like a HAL document, showing the *self* link to `localhost:8080/employees` that you declared.
|
|
* The *_templates* section is the HAL-FORMS extension that shows the *affordance* defined that pointed to the `newEmployee` method, which was mapped onto the *POST* method.
|
|
** Inside the template, the method is clearly marked *post*.
|
|
** The properties are: *firstName*, *id*, *lastName*, and *role*, and all marked as *required*.
|
|
** The other characteristics (title, contentType) are not filled out. There are more attributes, but nothing (yet) that can be gleaned from a plain old Spring MVC route.
|
|
|
|
This template data is enough information for you to generate an HTML form on a web page using a little JavaScript. Possibly one like this:
|
|
|
|
[source,html]
|
|
----
|
|
<form method="post" action="http://localhost:8080/employees">
|
|
<input type="text" id="firstName" name="firstName" placeHolder="firstName" />
|
|
<input type="text" id="id" name="id" placeHolder="id" />
|
|
<input type="text" id="lastName" name="lastName" placeHolder="lastName" />
|
|
<input type="text" id="role" name="role" placeHolder="role" />
|
|
<input type="submit" value="Submit" />
|
|
</form>
|
|
----
|
|
|
|
You can also define affordances at the individual resource level. In this situation, you can start first by defining the controller methods:
|
|
|
|
[source,java]
|
|
----
|
|
@RestController
|
|
class EmployeeController {
|
|
|
|
...
|
|
|
|
@GetMapping("/employees/{id}")
|
|
ResponseEntity<Resource<Employee>> findOne(@PathVariable long id) {
|
|
|
|
return repository.findById(id)
|
|
.map(assembler::toResource)
|
|
.map(ResponseEntity::ok)
|
|
.orElse(ResponseEntity.notFound().build());
|
|
}
|
|
|
|
@PutMapping("/employees/{id}")
|
|
ResponseEntity<?> updateEmployee(@RequestBody Employee employee, @PathVariable long id) {
|
|
|
|
Employee employeeToUpdate = employee;
|
|
employeeToUpdate.setId(id);
|
|
|
|
return repository.save(employeeToUpdate).getId()
|
|
.map(this::findOne)
|
|
.map(HttpEntity::getBody)
|
|
.flatMap(ResourceSupport::getId)
|
|
.map(Link::getHref)
|
|
.map(href -> {
|
|
try {
|
|
return new URI(href);
|
|
} catch (URISyntaxException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
})
|
|
.map(uri -> ResponseEntity.noContent().location(uri).build())
|
|
.orElse(ResponseEntity.badRequest().body("Unable to update " + employeeToUpdate));
|
|
}
|
|
|
|
...
|
|
}
|
|
----
|
|
|
|
This augments the same REST controller with a *GET* operation for an individual `Employee` and also defines the corresponding *PUT* operation
|
|
to update/edit.
|
|
|
|
Take your team to read both flows. The key part you must define, is the corresponding `EmployeeResourceAssembler.toResource(Employee)` method.
|
|
In the heart of that method, is its `addLinks(Resource<Employee> resource)` method:
|
|
|
|
[source,java]
|
|
----
|
|
@Component
|
|
class EmployeeResourceAssembler extends SimpleIdentifiableResourceAssembler<Employee> {
|
|
|
|
...
|
|
|
|
/**
|
|
* Define links to add to every {@link Resource}.
|
|
*
|
|
* @param resource
|
|
*/
|
|
@Override
|
|
protected void addLinks(Resource<Employee> resource) {
|
|
|
|
resource.getContent().getId()
|
|
.ifPresent(id -> resource.add(getCollectionLinkBuilder().slash(resource.getContent()).withSelfRel()
|
|
.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, id)))));
|
|
|
|
resource.add(getCollectionLinkBuilder().withRel(this.getRelProvider().getCollectionResourceRelFor(this.getResourceType())));
|
|
}
|
|
|
|
...
|
|
}
|
|
----
|
|
|
|
In this situation, you are taking an individual `Resource<Employee>`, extracting the contents (i.e. the `Employee` itself), grabbing
|
|
its `Optional` *id*, and if present, creating a *self* link to it. Again, because `Employee` implements `Identifiable`, `slash(Employee)`
|
|
is able to extract the *id* field to form a proper link. This defines the *GET* endpoint as a Spring HATEOAS `Link`.
|
|
|
|
Using this *self* link, you then add an affordance to the controller's `updateEmployee` method. Spring HATEOAS's Affordances API
|
|
is able to inspect the Spring MVC annotations and extract information to render a HAL-FORMS endpoint.
|
|
|
|
If you restart the application and ping `/employees/1`, you can see an individual entry:
|
|
|
|
[source,javascript]
|
|
----
|
|
{
|
|
"id": 1,
|
|
"firstName": "Frodo",
|
|
"lastName": "Baggins",
|
|
"role": "ring bearer",
|
|
"_links": {
|
|
"self": {
|
|
"href": "http://localhost:8080/employees/1"
|
|
},
|
|
"employees": {
|
|
"href": "http://localhost:8080/employees"
|
|
}
|
|
},
|
|
"_templates": {
|
|
"default": {
|
|
"title": null,
|
|
"method": "put",
|
|
"contentType": "",
|
|
"properties": [
|
|
{
|
|
"name": "firstName",
|
|
"required": true
|
|
},
|
|
{
|
|
"name": "id",
|
|
"required": true
|
|
},
|
|
{
|
|
"name": "lastName",
|
|
"required": true
|
|
},
|
|
{
|
|
"name": "role",
|
|
"required": true
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
----
|
|
|
|
* This is very similar to what you saw before, only there is no *_embedded* element. Instead, the resource's data is at the top level.
|
|
* There are two links: *self* for the canonical link to itself and *employees* to lead back to the aggregate root.
|
|
* The method of this template is *put* instead of *post*, indicating this is for updates.
|
|
* All the properties are listed, being the same as shown at the aggregate root.
|
|
|
|
This information could _also_ be used on your web site to generate update forms:
|
|
|
|
[source,html]
|
|
----
|
|
<form method="put" action="http://localhost:8080/employees/1">
|
|
<input type="text" id="firstName" name="firstName" placeHolder="firstName" />
|
|
<input type="text" id="id" name="id" placeHolder="id" />
|
|
<input type="text" id="lastName" name="lastName" placeHolder="lastName" />
|
|
<input type="text" id="role" name="role" placeHolder="role" />
|
|
<input type="submit" value="Submit" />
|
|
</form>
|
|
----
|
|
|
|
This is just one example of an update form.
|
|
|
|
NOTE: `method="put"` isn't exactly valid HTML5. Either you can handle that in your code, or you have some sort of filter like Spring MVC's
|
|
`HiddenHttpMethodFilter` that lets you construct it as `<form method="post" _method="put" ...>`, which converts a *POST* into a *PUT* before
|
|
invoking the code.
|
|
|
|
IMPORTANT: With HAL-FORMS, there is no URI in the template itself. It's presumed to operate on the *self* link.
|
|
|
|
With the Affordances API, you can link related methods. And with HAL-FORMS support, it's possible to turn those relationships into automated
|
|
bits of HTML to enhance the user experience without having to inject domain knowledge into the client layer.
|
|
|
|
And that's a key part of REST--reducing the amount of domain knowledge found in the client, allowing the client to more easily adapt to
|
|
changes on the server. |