272 lines
9.7 KiB
Plaintext
272 lines
9.7 KiB
Plaintext
= Spring HATEOAS - Basic Example
|
|
|
|
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 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
|
|
@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(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
|
|
* `@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.
|
|
|
|
== 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 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.
|
|
|
|
[source,java]
|
|
----
|
|
@Component
|
|
class EmployeeResourceAssembler 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}.
|
|
*/
|
|
EmployeeResourceAssembler() {
|
|
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.
|
|
|
|
== Creating Links
|
|
|
|
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;
|
|
private final EmployeeResourceAssembler assembler;
|
|
|
|
EmployeeController(EmployeeRepository repository, EmployeeResourceAssembler 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
|
|
`EmployeeResourceAssembler` 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:
|
|
|
|
[source,java]
|
|
----
|
|
/**
|
|
* Look up all employees, and transform them into a REST collection resource using
|
|
* {@link EmployeeResourceAssembler#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 `EmployeeResourceAssembler` 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:
|
|
|
|
[source,java]
|
|
----
|
|
/**
|
|
* Look up a single {@link Employee} and transform it into a REST resource using
|
|
* {@link EmployeeResourceAssembler#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 ResponseEntity.ok(
|
|
assembler.toEntityModel(repository.findOne(id)));
|
|
}
|
|
----
|
|
|
|
Again, the `EmployeeResourceAssembler` is used to convert a single `Employee` into a `EntityModel<Employee>`
|
|
through its `toEntityModel(Employee)` method.
|
|
|
|
== Customizing the Output
|
|
|
|
What's not shown in this example is that the `EmployeeResourceAssembler` comes with overrides.
|
|
|
|
* `setBasePath(/* base */)` would inject a prefix into every link built in the hypermedia.
|
|
* `addLinks(EntityModel<T>)` and `addLinks(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.
|
|
|
|
== 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)
|
|
@Import({EmployeeResourceAssembler.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({EmployeeResourceAssembler.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` flags `EmployeeRepositor` as a test collaborator.
|
|
|
|
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_VALUE + ";charset=UTF-8"))
|
|
.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.
|
|
** Verify that the JSON Path of *$._embedded.employees[0].id* is `1`.
|
|
|
|
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]. |