|
| 1 | += Spring HATEOAS - Basic Example |
| 2 | + |
| 3 | +This guides shows how to add Spring HATEOAS in the simplest way possible. Like the rest of these examples, it uses a payroll system. |
| 4 | + |
| 5 | +NOTE: This example uses https://projectlombok.org[Project Lombok] to reduce writing Java code. |
| 6 | + |
| 7 | +== Defining Your Domain |
| 8 | + |
| 9 | +The cornerstone of any example is the domain object: |
| 10 | + |
| 11 | +[source,java] |
| 12 | +---- |
| 13 | +@Data |
| 14 | +@Entity |
| 15 | +@NoArgsConstructor(access = AccessLevel.PRIVATE) |
| 16 | +@AllArgsConstructor |
| 17 | +class Employee { |
| 18 | +
|
| 19 | + @Id @GeneratedValue |
| 20 | + private Long id; |
| 21 | + private String firstName; |
| 22 | + private String lastName; |
| 23 | + private String role; |
| 24 | +
|
| 25 | + ... |
| 26 | +} |
| 27 | +---- |
| 28 | + |
| 29 | +This domain object includes: |
| 30 | + |
| 31 | +* `@Data` - Lombok annotation to define a mutable value object |
| 32 | +* `@Entity` - JPA annotation to make the object storagable in a classic SQL engine (H2 in this example) |
| 33 | +* `@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. |
| 34 | +* `@AllArgsConstructor` - Lombok annotation to create an all-arg constructor for certain test scenarios |
| 35 | + |
| 36 | +== Accessing Data |
| 37 | + |
| 38 | +To experiment with something realistic, you need to access a real database. This example leverages H2, an embedded JPA datasource. |
| 39 | +And while it's not a requirement for Spring HATEOAS, this example uses Spring Data JPA. |
| 40 | + |
| 41 | +Create a repository like this: |
| 42 | + |
| 43 | +[source,java] |
| 44 | +---- |
| 45 | +interface EmployeeRepository extends CrudRepository<Employee, Long> { |
| 46 | +} |
| 47 | +---- |
| 48 | + |
| 49 | +This interface extends Spring Data Commons' `CrudRepository`, inheriting a collection of create/replace/update/delete (CRUD) |
| 50 | +operations. |
| 51 | + |
| 52 | +[[converting-entities-to-resources]] |
| 53 | +== Converting Entities to Resources |
| 54 | + |
| 55 | +In REST, the "thing" being linked to is a *resource*. Resources provide both information as well as details on _how_ to |
| 56 | +retrieve and update that information. |
| 57 | + |
| 58 | +Spring HATEOAS defines a generic `Resource<T>` container that lets you store any domain object (`Employee` in this example), and |
| 59 | +add additional links. |
| 60 | + |
| 61 | +IMPORTANT: Spring HATEOAS's `Resource` and `Link` classes are *vendor neutral*. HAL is thrown around a lot, being the |
| 62 | +default mediatype, but these classes can be used to render any mediatype. |
| 63 | + |
| 64 | +The following Spring MVC controller defines the application's routes, and hence is the source of links needed |
| 65 | +in the hypermedia. |
| 66 | + |
| 67 | +NOTE: This guide assumes you already somewhat familiar with Spring MVC. |
| 68 | + |
| 69 | +[source,java] |
| 70 | +---- |
| 71 | +@RestController |
| 72 | +class EmployeeController { |
| 73 | +
|
| 74 | + private final EmployeeRepository repository; |
| 75 | +
|
| 76 | + EmployeeController(EmployeeRepository repository) { |
| 77 | + this.repository = repository; |
| 78 | + } |
| 79 | +
|
| 80 | + ... |
| 81 | +} |
| 82 | +---- |
| 83 | + |
| 84 | +This piece of code shows how the Spring MVC controller is wired with a copy of the `EmployeeRepository` through |
| 85 | +constructor injection and marked as a *REST controller* thanks to the `@RestController` annotation. |
| 86 | + |
| 87 | +The route for the https://martinfowler.com/bliki/DDD_Aggregate.html[aggregate root] is shown below: |
| 88 | + |
| 89 | +[source,java] |
| 90 | +---- |
| 91 | +/** |
| 92 | + * Look up all employees, and transform them into a REST collection resource. |
| 93 | + * Then return them through Spring Web's {@link ResponseEntity} fluent API. |
| 94 | + * |
| 95 | + * NOTE: cURL will fetch things as HAL JSON directly, but browsers issue a different |
| 96 | + * default accept header, which allows XML to get requested first, so "produces" |
| 97 | + * forces it to HAL JSON for all clients. |
| 98 | + */ |
| 99 | +@GetMapping(value = "/employees", produces = MediaTypes.HAL_JSON_VALUE) |
| 100 | +ResponseEntity<Resources<Resource<Employee>>> findAll() { |
| 101 | +
|
| 102 | + List<Resource<Employee>> employees = StreamSupport.stream(repository.findAll().spliterator(), false) |
| 103 | + .map(employee -> new Resource<>(employee, |
| 104 | + linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(), |
| 105 | + linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"))) |
| 106 | + .collect(Collectors.toList()); |
| 107 | +
|
| 108 | + return ResponseEntity.ok( |
| 109 | + new Resources<>(employees, |
| 110 | + linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel())); |
| 111 | +} |
| 112 | +---- |
| 113 | + |
| 114 | +It retrieves a collection of `Employee` objects, streams through a Java 8 spliterator, and converts them into a collection |
| 115 | +of `Resource<Employee>` objects by using Spring HATEOAS's `linkTo` and `methodOn` helpers to build links. |
| 116 | + |
| 117 | +* The natural convention with REST endpoints is to serve a *self* link (denoted by the `.withSelfRel()` call). |
| 118 | +* It's also useful for any single item resource to include a link back to the aggregate (denoted by the `.withRel("employees")`). |
| 119 | + |
| 120 | +The whole collection of single item resources is then wrapped in a Spring HATEOAS `Resources` type. |
| 121 | + |
| 122 | +NOTE: `Resources` is Spring HATEOAS's vendor neutral representation of a collection. It has it's |
| 123 | +own set of links, separate from the links of each member of the collection. That's why the whole |
| 124 | +structure is `Resources<Resource<Employee>>` and not `Resources<Employee>`. |
| 125 | + |
| 126 | +To build a single resource, the `/employees/{id}` route is shown below: |
| 127 | + |
| 128 | +[source,java] |
| 129 | +---- |
| 130 | +/** |
| 131 | + * Look up a single {@link Employee} and transform it into a REST resource. Then return it through |
| 132 | + * Spring Web's {@link ResponseEntity} fluent API. |
| 133 | + * |
| 134 | + * See {@link #findAll()} to explain {@link GetMapping}'s "produces" argument. |
| 135 | + * |
| 136 | + * @param id |
| 137 | + */ |
| 138 | +@GetMapping(value = "/employees/{id}", produces = MediaTypes.HAL_JSON_VALUE) |
| 139 | +ResponseEntity<Resource<Employee>> findOne(@PathVariable long id) { |
| 140 | +
|
| 141 | + return repository.findById(id) |
| 142 | + .map(employee -> new Resource<>(employee, |
| 143 | + linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(), |
| 144 | + linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"))) |
| 145 | + .map(ResponseEntity::ok) |
| 146 | + .orElse(ResponseEntity.notFound().build()); |
| 147 | +} |
| 148 | +---- |
| 149 | + |
| 150 | +This code is almost identical. It fetches a single item `Employee` from the database and that wraps up into a |
| 151 | +`Resource<Employee>` object with the same links, but that's it. No need to create a `Resources` object since is NOT a |
| 152 | +collection. |
| 153 | + |
| 154 | +IMPORTANT: Does this look like duplicate code found in the aggregate root? Sures it does. That's why Spring HATEOAS |
| 155 | + includes the ability to define a `ResourceAssembler`. It lets you define, in one place, all the links for a given |
| 156 | + entity type. Then you can reuse it as needed in all relevant controller methods. It's been left out of this section |
| 157 | + for the sake of simplicity. |
| 158 | + |
| 159 | +== Testing Hypermedia |
| 160 | + |
| 161 | +Nothing is complete without testing. Thanks to Spring Boot, it's easier than ever to test a Spring MVC controller, |
| 162 | +including the generated hypermedia. |
| 163 | + |
| 164 | +The following is a bare bones "slice" test case: |
| 165 | + |
| 166 | +[source,java] |
| 167 | +---- |
| 168 | +@RunWith(SpringRunner.class) |
| 169 | +@WebMvcTest(EmployeeController.class) |
| 170 | +public class EmployeeControllerTests { |
| 171 | +
|
| 172 | + @Autowired |
| 173 | + private MockMvc mvc; |
| 174 | +
|
| 175 | + @MockBean |
| 176 | + private EmployeeRepository repository; |
| 177 | +
|
| 178 | + ... |
| 179 | +} |
| 180 | +---- |
| 181 | + |
| 182 | +* `@RunWith(SpringRunner.class)` is needed to leverage Spring Boot's test annotations with JUnit. |
| 183 | +* `@WebMvcTest(EmployeeController.class)` confines Spring Boot to only autoconfiguring Spring MVC components, and _only_ |
| 184 | +this one controller, making it a very precise test case. |
| 185 | +* `@Autowired MockMvc` gives us a handle on a Spring Mock tester. |
| 186 | +* `@MockBean` flags `EmployeeRepository` as a test collaborator, since we don't plan on talking to a real database in this test case. |
| 187 | + |
| 188 | +With this structure, we can start crafting a test case! |
| 189 | + |
| 190 | +[source,java] |
| 191 | +---- |
| 192 | +@Test |
| 193 | +public void getShouldFetchAHalDocument() throws Exception { |
| 194 | +
|
| 195 | + given(repository.findAll()).willReturn( |
| 196 | + Arrays.asList( |
| 197 | + new Employee(1L,"Frodo", "Baggins", "ring bearer"), |
| 198 | + new Employee(2L,"Bilbo", "Baggins", "burglar"))); |
| 199 | +
|
| 200 | + mvc.perform(get("/employees").accept(MediaTypes.HAL_JSON_VALUE)) |
| 201 | + .andDo(print()) |
| 202 | + .andExpect(status().isOk()) |
| 203 | + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_UTF8_VALUE)) |
| 204 | + .andExpect(jsonPath("$._embedded.employees[0].id", is(1))) |
| 205 | + ... |
| 206 | +} |
| 207 | +---- |
| 208 | + |
| 209 | +* At first, the test case uses Mockito's `given()` method to define the "given"s of the test. |
| 210 | +* Next, it uses Spring Mock MVC's `mvc` to `perform()` a *GET /employees* call with an accept header of HAL's mediatype. |
| 211 | +* As a courtesy, it uses the `.andDo(print())` to give us a complete print out of the whole thing on the console. |
| 212 | +* Finally, it chains a whole series of assertions. |
| 213 | +** Verify HTTP status is *200 OK*. |
| 214 | +** Verify the response *Content-Type* header is also HAL's mediatype (with UTF-8 flavor). |
| 215 | +** Verify that the JSON Path of *$._embedded.employees[0].id* is `1`. |
| 216 | +** And so forth... |
| 217 | + |
| 218 | +The rest of the assertions are commented out, but you can read it in the source code. |
| 219 | + |
| 220 | +NOTE: This is not the only way to assert the results. See Spring Framework reference docs and Spring HATEOAS |
| 221 | +test cases for more examples. |
| 222 | + |
| 223 | +For the next step in Spring HATEOAS, you may wish to read link:../api-evolution[Spring HATEOAS - API Evolution Example]. |
0 commit comments