= Spring API - Best Practices
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 https://projectlombok.org[Project Lombok] to reduce writing Java code.
== Defining Your Domain
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
== Accessing Data
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:
This interface extends Spring Data Commons' CrudRepository
, inheriting a collection of create/replace/update/delete (CRUD)
operations.
[[converting-entities-to-resources]] == Converting Entities to Resources
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 https://martinfowler.com/bliki/DDD_Aggregate.html[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>> findAll() {
List<EntityModel> 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> 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.
== Testing Hypermedia
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.