This guides shows a core example of using Spring HATEOAS. It illustrates how to sew hypermedia into your Spring MVC application, including test.
Start with a very simple example, a payroll system that tracks employees.
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
@JsonIgnoreProperties(ignoreUnknown = true)
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(PROTECTED)
- Lombok annotation to create an empty constructor call to appease JPA, but which is protected and not usable to our app’s code. -
@AllArgsConstructor
- Lombok annotation to create an all-arg constructor for certain test scenarios -
@JsonIgnoreProperties(ignoreUnknown=true)
- Jackson annotation to ignore unknown attributes when deserializing JSON.
Note
|
Make your REST resources robust by instructing Jackson to ignore unknown attributes. That way, additional elements, like hypermedia (HAL _links, HAL-Forms _templates, etc.) and newer attributes will be discarded. |
This means a HAL document like this:
{ "firstName": "Frodo", "lastName": "Baggins", "role" : "ring bearer", "_links" : { "self" : { "href" : "/employees/1" } } }
…which was served up as hypermedia, can be POST’d to create a new employee. The _links will simply be ignored.
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 information on how to retrieve and update that information.
Spring HATEOAS defines a generic EntityModel<T>
container used to model resources that lets store any domain object (Employee
in this example), and
add additional links.
Important
|
Spring HATEOAS’s EntityModel 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.
|
A common convention is to create a factory used to convert Employee
objects into EntityModel<Employee>
objects. Spring
HATEOAS provides SimpleIdentifiableRepresentationModelAssembler<T>
as the simplest mechanism to perform these conversions.
@Component
class EmployeeRepresentationModelAssembler extends SimpleIdentifiableRepresentationModelAssembler<Employee> {
/**
* Link the {@link Employee} domain type to the {@link EmployeeController} using this
* {@link SimpleIdentifiableRepresentationModelAssembler} in order to generate both {@link org.springframework.hateoas.EntityModel}
* and {@link org.springframework.hateoas.CollectionModel}.
*/
EmployeeRepresentationModelAssembler() {
super(EmployeeController.class);
}
}
This class has two key properties:
-
The generic type (
Employee
) declares the entity type. -
The constructor defines the Spring MVC controller (
EmployeeController
) where links are found to turn the entity into a REST resource.
The class is flagged with Spring’s @Component
annotation so that it will be automatically hooked into the
application context.
This is half the battle, already solved. This resource assembler is used in the controller to assemble REST resources, as shown in the next section.
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;
private final EmployeeRepresentationModelAssembler assembler;
EmployeeController(EmployeeRepository repository, EmployeeRepresentationModelAssembler assembler) {
this.repository = repository;
this.assembler = assembler;
}
...
}
This piece of code shows how the Spring MVC controller is wired with a copy of the EmployeeRepository
as well as a
EmployeeRepresentationModelAssembler
and marked as a REST controller thanks to the @RestController
annotation.
To support SimpleIdentifiableRepresentationModelAssembler
, the controller needs two things:
-
A route to the collection. By default, it assumes a pluralized, lowercased name (
Employee
→/employees
). -
A route to a single entity. By default it assumes the collection’s URI +
/{id}
.
The collection’s route is shown below:
/**
* Look up all employees, and transform them into a REST collection resource using
* {@link EmployeeRepresentationModelAssembler#toCollectionModel(Iterable)}. Then return them through
* Spring Web's {@link ResponseEntity} fluent API.
*/
@GetMapping("/employees")
public ResponseEntity<CollectionModel<EntityModel<Employee>>> findAll() {
return ResponseEntity.ok(
assembler.toCollectionModel(repository.findAll()));
}
It uses the EmployeeRepresentationModelAssembler
and it’s toCollectionModel(Iterable<Employee>)
method to turn a collection of
Employee
objects into a CollectionModel<EntityModel<Employee>>
.
Note
|
CollectionModel 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 using
* {@link EmployeeRepresentationModelAssembler#toEntityModel(Object)}. Then return it through
* Spring Web's {@link ResponseEntity} fluent API.
*
* @param id
*/
@GetMapping("/employees/{id}")
public ResponseEntity<EntityModel<Employee>> findOne(@PathVariable long id) {
return this.repository.findById(id) //
.map(this.assembler::toModel) //
.map(ResponseEntity::ok) //
.orElse(ResponseEntity.notFound().build());
}
Again, the EmployeeRepresentationModelAssembler
is used to convert a single Employee
into a EntityModel<Employee>
through its toEntityModel(Employee)
method.
What’s not shown in this example is that the EmployeeRepresentationModelAssembler
comes with overrides.
-
setBasePath(/* base */)
would inject a prefix into every link built in the hypermedia. -
addLinks(EntityModel<T>)
andaddLinks(CollectionModel<T>)
allows you to override/augment the default links assigned to every resource. -
getCollectionLinkBuilder()
lets you override the convention of how the whole route is built up.
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)
@Import({EmployeeRepresentationModelAssembler.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. -
@Import({EmployeeRepresentationModelAssembler.class})
pulls in one extra Spring component that would be ignored by@WebMvcTest
. -
@Autowired MockMvc
gives us a handle on a Spring Mock tester. -
@MockBean
flagsEmployeeRepositor
as a test collaborator.
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_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.
-
Verify that the JSON Path of $._embedded.employees[0].id is
1
.
-
The rest of the assertions are not shown above, 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.