Write guide for 'basic' example
This commit is contained in:
279
basics/README.adoc
Normal file
279
basics/README.adoc
Normal file
@@ -0,0 +1,279 @@
|
||||
= 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
|
||||
@AllArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
class Employee implements Identifiable<Long> {
|
||||
|
||||
@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` - Lombok annotation to create an empty constructor call to appear Jackson.
|
||||
* `@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.
|
||||
|
||||
It also implements `Identifiable<Long>`, which comes with a single method, `getId`, used to fetch the unique identifier
|
||||
for a given entity.
|
||||
|
||||
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
|
||||
|
||||
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 `Resource<T>` container that lets 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 mediatype, but the classes you are using can be used to render any mediatype.
|
||||
|
||||
A common convention is to create a factory used to convert `Employee` objects into `Resource<Employee>` objects. Spring
|
||||
HATEOAS provides `SimpleIdentifiableResourceAssembler<T>` as the simplest mechanism to perform these conversions.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Component
|
||||
class EmployeeResourceAssembler extends SimpleIdentifiableResourceAssembler<Employee> {
|
||||
|
||||
/**
|
||||
* Link the {@link Employee} domain type to the {@link EmployeeController} using this
|
||||
* {@link SimpleIdentifiableResourceAssembler} in order to generate both {@link org.springframework.hateoas.Resource}
|
||||
* and {@link org.springframework.hateoas.Resources}.
|
||||
*/
|
||||
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, flagging it to be automatically hooked into the
|
||||
application context.
|
||||
|
||||
|
||||
This is half the battle. 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 `SimpleIdentifiableResourceAssembler`, 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#toResources(Iterable)}. Then return them through
|
||||
* Spring Web's {@link ResponseEntity} fluent API.
|
||||
*
|
||||
* NOTE: cURL will fetch things as HAL JSON directly, but browsers issue a different
|
||||
* default accept header, which allows XML to get requested first, so "produces"
|
||||
* forces it to HAL JSON for all clients.
|
||||
*/
|
||||
@GetMapping(value = "/employees", produces = MediaTypes.HAL_JSON_VALUE)
|
||||
public ResponseEntity<Resources<Resource<Employee>>> findAll() {
|
||||
return ResponseEntity.ok(
|
||||
assembler.toResources(repository.findAll()));
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
It uses the `EmployeeResourceAssembler` and it's `toResources(Employee)` method to turn a collection of
|
||||
`Employee` objects into a `Resources<Resource<Employee>>`.
|
||||
|
||||
NOTE: `Resources` is Spring HATEOAS's vendor neutral representation of a collection. It has it's
|
||||
own collection of links, separate from the links of every member of the collection. That's why the whole
|
||||
structure is `Resources<Resource<Employee>>` and not `Resources<Employee>`.
|
||||
|
||||
To build a single resource, the `/employess/{id}` route is shown below:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
/**
|
||||
* Look up a single {@link Employee} and transform it into a REST resource using
|
||||
* {@link EmployeeResourceAssembler#toResource(Object)}. Then return it through
|
||||
* Spring Web's {@link ResponseEntity} fluent API.
|
||||
*
|
||||
* See {@link #findAll()} to explain {@link GetMapping}'s "produces" argument.
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
@GetMapping(value = "/employees/{id}", produces = MediaTypes.HAL_JSON_VALUE)
|
||||
public ResponseEntity<Resource<Employee>> findOne(@PathVariable long id) {
|
||||
return ResponseEntity.ok(
|
||||
assembler.toResource(repository.findOne(id)));
|
||||
}
|
||||
----
|
||||
|
||||
Again, the `EmployeeResourceAssembler` is used to convert a single `Employee` into a `Resource<Employee>`
|
||||
through its `toResource(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(Resource<T>)` and `addLinks(Resources<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 mediatype.
|
||||
* 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 mediatype.
|
||||
** 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.
|
||||
@@ -65,7 +65,7 @@ class EmployeeController {
|
||||
* Spring Web's {@link ResponseEntity} fluent API.
|
||||
*
|
||||
* See {@link #findAll()} to explain {@link GetMapping}'s "produces" argument.
|
||||
*
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
@GetMapping(value = "/employees/{id}", produces = MediaTypes.HAL_JSON_VALUE)
|
||||
|
||||
Reference in New Issue
Block a user