217 lines
8.1 KiB
Plaintext
217 lines
8.1 KiB
Plaintext
= Spring HATEOAS - Basic Example
|
|
|
|
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:
|
|
|
|
[source,java]
|
|
----
|
|
@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:
|
|
|
|
[source,java]
|
|
----
|
|
interface EmployeeRepository extends CrudRepository<Employee, Long> {
|
|
}
|
|
----
|
|
|
|
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.
|
|
|
|
[source,java]
|
|
----
|
|
@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:
|
|
|
|
[source,java]
|
|
----
|
|
/**
|
|
* 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:
|
|
|
|
[source,java]
|
|
----
|
|
/**
|
|
* 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.
|
|
|
|
== 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:
|
|
|
|
[source,java]
|
|
----
|
|
@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` flags `EmployeeRepository` 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!
|
|
|
|
[source,java]
|
|
----
|
|
@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` to `perform()` 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 link:../api-evolution[Spring HATEOAS - API Evolution Example]. |