This guides shows how to add Spring HATEOAS in the simplest way possible. Like the rest of these examples, it uses a payroll system.
Note
|
This example uses Project Lombok to reduce writing Java code. |
The cornerstone of any example is the domain object:
@Data
@Entity
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
class Employee {
@Id @GeneratedValue
private Long id;
private String firstName;
private String lastName;
private String role;
...
}
This domain object includes:
-
@Data
- Lombok annotation to define a mutable value object -
@Entity
- JPA annotation to make the object storagable in a classic SQL engine (H2 in this example) -
@NoArgsConstructor(PRIVATE)
- Lombok annotation to create an empty constructor call to appease Jackson, but which is private and not usable to our app’s code. -
@AllArgsConstructor
- Lombok annotation to create an all-arg constructor for certain test scenarios
To experiment with something realistic, you need to access a real database. This example leverages H2, an embedded JPA datasource. And while it’s not a requirement for Spring HATEOAS, this example uses Spring Data JPA.
Create a repository like this:
interface EmployeeRepository extends CrudRepository<Employee, Long> {
}
This interface extends Spring Data Commons' CrudRepository
, inheriting a collection of create/replace/update/delete (CRUD)
operations.
In REST, the "thing" being linked to is a resource. Resources provide both information as well as details on how to retrieve and update that information.
Spring HATEOAS defines a generic EntityModel<T>
container that lets you store any domain object (Employee
in this example), and
add additional links.
Important
|
Spring HATEOAS’s Resource and Link classes are vendor neutral. HAL is thrown around a lot, being the
default media type, but these classes can be used to render any media type.
|
The following Spring MVC controller defines the application’s routes, and hence is the source of links needed in the hypermedia.
Note
|
This guide assumes you already somewhat familiar with Spring MVC. |
@RestController
class EmployeeController {
private final EmployeeRepository repository;
EmployeeController(EmployeeRepository repository) {
this.repository = repository;
}
...
}
This piece of code shows how the Spring MVC controller is wired with a copy of the EmployeeRepository
through
constructor injection and marked as a REST controller thanks to the @RestController
annotation.
The route for the aggregate root is shown below:
/**
* Look up all employees, and transform them into a REST collection resource.
* Then return them through Spring Web's {@link ResponseEntity} fluent API.
*/
@GetMapping("/employees")
ResponseEntity<CollectionModel<EntityModel<Employee>>> findAll() {
List<EntityModel<Employee>> employees = StreamSupport.stream(repository.findAll().spliterator(), false)
.map(employee -> EntityModel.of(employee,
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(),
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")))
.collect(Collectors.toList());
return ResponseEntity.ok(
CollectionModel.of(employees,
linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel()));
}
It retrieves a collection of Employee
objects, streams through a Java 8 spliterator, and converts them into a collection
of EntityModel<Employee>
objects by using Spring HATEOAS’s linkTo
and methodOn
helpers to build links.
-
The natural convention with REST endpoints is to serve a self link (denoted by the
.withSelfRel()
call). -
It’s also useful for any single item resource to include a link back to the aggregate (denoted by the
.withRel("employees")
).
The whole collection of single item resources is then wrapped in a Spring HATEOAS Resources
type.
Note
|
Resources is Spring HATEOAS’s vendor neutral representation of a collection. It has it’s
own set of links, separate from the links of each member of the collection. That’s why the whole
structure is CollectionModel<EntityModel<Employee>> and not CollectionModel<Employee> .
|
To build a single resource, the /employees/{id}
route is shown below:
/**
* Look up a single {@link Employee} and transform it into a REST resource. Then return it through
* Spring Web's {@link ResponseEntity} fluent API.
*
* @param id
*/
@GetMapping("/employees/{id}")
ResponseEntity<EntityModel<Employee>> findOne(@PathVariable long id) {
return repository.findById(id)
.map(employee -> EntityModel.of(employee,
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(),
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")))
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
This code is almost identical. It fetches a single item Employee
from the database and that wraps up into a
EntityModel<Employee>
object with the same links, but that’s it. No need to create a Resources
object since is NOT a
collection.
Important
|
Does this look like duplicate code found in the aggregate root? Sures it does. That’s why Spring HATEOAS
includes the ability to define a ResourceAssembler . It lets you define, in one place, all the links for a given
entity type. Then you can reuse it as needed in all relevant controller methods. It’s been left out of this section
for the sake of simplicity.
|
Nothing is complete without testing. Thanks to Spring Boot, it’s easier than ever to test a Spring MVC controller, including the generated hypermedia.
The following is a bare bones "slice" test case:
@RunWith(SpringRunner.class)
@WebMvcTest(EmployeeController.class)
public class EmployeeControllerTests {
@Autowired
private MockMvc mvc;
@MockBean
private EmployeeRepository repository;
...
}
-
@RunWith(SpringRunner.class)
is needed to leverage Spring Boot’s test annotations with JUnit. -
@WebMvcTest(EmployeeController.class)
confines Spring Boot to only autoconfiguring Spring MVC components, and only this one controller, making it a very precise test case. -
@Autowired MockMvc
gives us a handle on a Spring Mock tester. -
@MockBean
flagsEmployeeRepository
as a test collaborator, since we don’t plan on talking to a real database in this test case.
With this structure, we can start crafting a test case!
@Test
public void getShouldFetchAHalDocument() throws Exception {
given(repository.findAll()).willReturn(
Arrays.asList(
new Employee(1L,"Frodo", "Baggins", "ring bearer"),
new Employee(2L,"Bilbo", "Baggins", "burglar")));
mvc.perform(get("/employees").accept(MediaTypes.HAL_JSON_VALUE))
.andDo(print())
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_UTF8_VALUE))
.andExpect(jsonPath("$._embedded.employees[0].id", is(1)))
...
}
-
At first, the test case uses Mockito’s
given()
method to define the "given"s of the test. -
Next, it uses Spring Mock MVC’s
mvc
toperform()
a GET /employees call with an accept header of HAL’s media type. -
As a courtesy, it uses the
.andDo(print())
to give us a complete print out of the whole thing on the console. -
Finally, it chains a whole series of assertions.
-
Verify HTTP status is 200 OK.
-
Verify the response Content-Type header is also HAL’s media type (with UTF-8 flavor).
-
Verify that the JSON Path of $._embedded.employees[0].id is
1
. -
And so forth…
-
The rest of the assertions are commented out, but you can read it in the source code.
Note
|
This is not the only way to assert the results. See Spring Framework reference docs and Spring HATEOAS test cases for more examples. |
For the next step in Spring HATEOAS, you may wish to read Spring HATEOAS - API Evolution Example.