Merikanto

一簫一劍平生意,負盡狂名十五年

Spring Boot - 06 Producing REST Services

This is the sixth post in a series of posts to cover different aspects of Spring Boot. Please note that the entire post isn’t necessarily only written in English.

In this post, I am going to cover how to define REST endpoints in Spring, and auto generate repository-based REST endpoints with Spring Data REST. This post describes how to produce REST services, and in the next post, we will talk about how to consume REST services.



Summary

  • Controller handler methods can either be annotated with @ResponseBody, or return ResponseEntity objects to bypass the model & view, and write data directly to the response body. However if we use @RestController, we don’t need these.

  • Spring HATEOAS enables hyperlinking of resources returned from Spring MVC controllers.

  • Spring Data repositories can automatically expose REST APIs via Spring Data REST.


Rest Controllers

Spring MVC creates traditional MPA (MultiPage Application), while Angular creates SPA (Single Page Application).

Since presentation is largely decoupled from backend processing in a SPA, it affords the opportunity to develop more than one UI (such as a native mobile application) for the same backend functionality. It can also integrate with other applications that can consume the API. But MPA is a simpler design if all you need is to display information on a web page.

The Angular client code will communicate with an API via HTTP requests.


Get

Angular code for GET:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Component, OnInit, Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { HttpClient } from '@angular/common/http';

@Component({
selector: 'recent-tacos',
templateUrl: 'recents.component.html',
styleUrls: ['./recents.component.css']
})

@Injectable()
export class RecentTacosComponent implements OnInit {

recentTacos: any;
constructor(private httpClient: HttpClient) { }

// Get recently designed tacos from server
// Note: HttpClient, subscribe
ngOnInit() {
this.httpClient.get('http://localhost:8080/design/recent')
.subscribe(data => this.recentTacos = data);
}
}

Controller to GET recent designs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@RestController

// 注意 produces,明确表示是 json
// Only handle requests if the Accept header includes "application/json"
@RequestMapping(path="/design", produces="application/json")

// Enable CORS: Cross Origin Resource Sharing
@CrossOrigin(origins="*")
public class DesignTacoController {

private TacoRepository tacoRepo;

@Autowired
EntityLinks entityLinks;

public DesignTacoController(TacoRepository tacoRepo) {
this.tacoRepo = tacoRepo;
}

@GetMapping("/recent")
public Iterable<Taco> recentTacos() {
PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());
return tacoRepo.findAll(page).getContent();
}
}

@RestController = @Controller + @ResponseBody

  • Marks a class for auto discovery by component scanning
  • All returned value should be written directly to the response body.

Offer an endpoint that GET a single taco by ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 注意: @PathVariable

@GetMapping("/{id}")
public Taco tacoById(@PathVariable("id") Long id) {

// id is passed to findById() in Repository
// Optional, because there may not be a taco with the given ID
Optional<Taco> optTaco = tacoRepo.findById(id);

if (optTaco.isPresent())
return optTaco.get();

return null;
}

Problem:

If we return null, the client receives a response with an empty body and 200 OK. The response cannot be used (since it’s empty), while the status indicates that everything is fine. So it’s better to return 404 for null.


Improvement: Use ResponseEntity to indicate status

1
2
3
4
5
6
7
8
9
10
@GetMapping("/{id}")
public ResponseEntity<Taco> tacoById(@PathVariable("id") Long id) {

Optional<Taco> optTaco = tacoRepo.findById(id);

if (optTaco.isPresent())
return new ResponseEntity<>(optTaco.get(), HttpStatus.OK);

return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
}

Post

Angular code for POST

1
2
3
4
5
6
7
8
9
// onSubmit calls httpClient.POST
onSubmit() {
this.httpClient.post('http://localhost:8080/design',
this.model, {
headers: new HttpHeaders().set('Content-type', 'application/json'),
}).subscribe(taco => this.cart.addToCart(taco));

this.router.navigate(['/cart']);
}

Controller to POST client input

1
2
3
4
5
6
7
8
// 注意,这里用的是 consumes,因为 receive input from client
@PostMapping(consumes="application/json")

// HTTP 201 CREATED, most descriptive than 200 OK
@ResponseStatus(HttpStatus.CREATED)
public Taco postTaco(@RequestBody Taco taco) {
return tacoRepo.save(taco);
}

@RequestBody: Ensures that JSON in the request body is bound to the Taco object. Without @RequestBody, Spring will bound request parameters to the Taco object.


@ResponseStatus: Communicate with more descriptive and accurate status to the client.


Put & Patch

It’s true that PUT is often used to update resource data, but PUT is actually the semantic opposite of GET . GET requests are for transferring data from the server to the client, and PUT requests are for sending data from the client to the server.

In a sense, PUT is really intended to perform a wholesale replacement operation rather than an update operation. In contrast, the purpose of PATCH is to perform a patch or partial update of resource data.

Suppose we want to change the order’s address. One way is to use a PUT request like this:

1
2
3
4
@PutMapping("/{orderId}")
public Order putOrder(@RequestBody Order order) {
return repo.save(order);
}

Note: The request path. This is the same way paths are handled by GET.

It would work, but it requires the client to submit the complete data in the PUT request. If any of the order’s properties are omitted, that property’s value would be overwritten to null.

In this case, we will use PATCH for a partial update.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@PatchMapping(path="/{orderId}", consumes="application/json")
public Order patchOrder(@PathVariable("orderId") Long orderId, @RequestBody Order patch) {

Order order = repo.findById(orderId).get();

if (patch.getDeliveryName() != null)
order.setDeliveryName(patch.getDeliveryName());

if (patch.getCcExpiration() != null)
order.setCcExpiration(patch.getCcExpiration());

......

return repo.save(order);
}

Even though PATCH semantically implies a partial update, it’s up to us to write code in the handler method that actually performs such an update. PATCH allows client to only send the properties that should be changed, and enables the server to retain other existing data.

However, PATCH has the following limitations:

  • If null means no change, then how will the client indicate that a field should be set to null?
  • There’s no way of removing / adding a subset of items in a collection. If client wants to modify a collection, the entire altered collection should be sent.

Delete

Delete an order:

1
2
3
4
5
6
7
8
9
10
11
12
13
@DeleteMapping("/{orderId}")

// Annotation! HTTP 204 No Content, but not 404!
@ResponseStatus(code=HttpStatus.NO_CONTENT)

// Note: public VOID!
public void deleteOrder(@PathVariable("orderId") Long orderId) {

try { repo.deleteById(orderId); }

// catch the exception
catch (EmptyResultDataAccessException e) {}
}

Why catch that exception and do nothing with it: If you delete a resource that doesn’t exist, the outcome is the same as deleting it, so whether it existed before or not doesn’t matter.

HTTP 204: Typically, DELETE requests have no body, and should only communicate the status code to let the client know not to expect any content.


Comparison

Compare 5 types of HTTP requests in the controller:

GET 1: Pagination

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Get recent designs
@RestController
@RequestMapping(path="/design", produces="application/json")
@CrossOrigin(origins="*")
public class DesignTacoController {

private TacoRepository tacoRepo;

public DesignTacoController(TacoRepository tacoRepo) {
this.tacoRepo = tacoRepo;
}

@GetMapping("/recent")
public Iterable<Taco> recentTacos() {
PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());
return tacoRepo.findAll(page).getContent();
}
}

GET 2: Response Entity

1
2
3
4
5
6
7
8
9
10
11
// Get signle taco by ID
@GetMapping("/{id}")
public ResponseEntity<Taco> tacoById(@PathVariable("id") Long id) {

Optional<Taco> optTaco = tacoRepo.findById(id);

if (optTaco.isPresent())
return new ResponseEntity<>(optTaco.get(), HttpStatus.OK);

return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
}
1
2
3
4
5
6
// Post client input
@PostMapping(consumes="application/json")
@ResponseStatus(HttpStatus.CREATED)
public Taco postTaco(@RequestBody Taco taco) {
return tacoRepo.save(taco);
}
1
2
3
4
@PutMapping("/{orderId}")
public Order putOrder(@RequestBody Order order) {
return repo.save(order);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@PatchMapping(path="/{orderId}", consumes="application/json")
public Order patchOrder(@PathVariable("orderId") Long orderId, @RequestBody Order patch) {

Order order = repo.findById(orderId).get();

if (patch.getDeliveryName() != null)
order.setDeliveryName(patch.getDeliveryName());

if (patch.getCcExpiration() != null)
order.setCcExpiration(patch.getCcExpiration());

......

return repo.save(order);
}
1
2
3
4
5
6
7
8
@DeleteMapping("/{orderId}")
@ResponseStatus(code=HttpStatus.NO_CONTENT)
public void deleteOrder(@PathVariable("orderId") Long orderId) {

try { repo.deleteById(orderId); }

catch (EmptyResultDataAccessException e) {}
}


Using Hypermedia

Hardcoding API URLs and using string manipulation on them makes the client code brittle. Hence HATEOAS (Hypermedia as the Engine of Application State) is for creating self-describing APIs, where resources returned from an API contain links to related
resources.

If the API is enabled with hypermedia, the API will describe its own URLs, relieving the client of needing to be hardcoded with that knowledge.

This enables clients to navigate an API with minimal understanding of the API’s URLs. Instead, it understands relationships between the resources served by the API, and uses understanding of relationships to discover the API’s URLs, as it traverses those relationships.


Getting Started

First, add HATEOAS to the dependency:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

A comparison between non-Hateoas & Hateoas:

The second one with hyperlinks is known as HAL (Hypertext Application Language), a simple and commonly used format for embedding hyperlinks in JSON responses.


Normal JSON:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[
{
"id": 4,
"name": "Veg-Out",
"createdAt": "2018-01-31T20:15:53.219+0000",
"ingredients": [
{"id": "FLTO", "name": "Flour Tortilla", "type": "WRAP"},
{"id": "COTO", "name": "Corn Tortilla", "type": "WRAP"},
{"id": "TMTO", "name": "Diced Tomatoes", "type": "VEGGIES"},
{"id": "LETC", "name": "Lettuce", "type": "VEGGIES"},
{"id": "SLSA", "name": "Salsa", "type": "SAUCE"}
]
},
...
]

Each element includes a property named _links that contains hyperlinks for the client to nagivate the API.

Should a client application need to perform an HTTP request against a taco in the list, it doesn’t need to be developed with any knowledge of the taco resource’s URL. Instead, it knows to ask for the self link, which maps to http://localhost:8080/design/4. If the client wants to deal with a particular ingredient, it only needs to follow the self link for that ingredient.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
{
"_embedded": {
"tacoResourceList": [
{
"name": "Veg-Out",
"createdAt": "2018-01-31T20:15:53.219+0000",
"ingredients": [
{
"name": "Flour Tortilla", "type": "WRAP",
"_links": {
"self": { "href": "http://localhost:8080/ingredients/FLTO" }
}
},
{
"name": "Corn Tortilla", "type": "WRAP",
"_links": {
"self": { "href": "http://localhost:8080/ingredients/COTO" }
}
},
{
"name": "Diced Tomatoes", "type": "VEGGIES",
"_links": {
"self": { "href": "http://localhost:8080/ingredients/TMTO" }
}
},
{
"name": "Lettuce", "type": "VEGGIES",
"_links": {
"self": { "href": "http://localhost:8080/ingredients/LETC" }
}
},
{
"name": "Salsa", "type": "SAUCE",
"_links": {
"self": { "href": "http://localhost:8080/ingredients/SLSA" }
}
}
],
"_links": {
"self": { "href": "http://localhost:8080/design/4" }
}
},
...
]
},
"_links": {
"recents": {
"href": "http://localhost:8080/design/recent" }
}
}

Spring HATEOAS provides two primary types that represent hyperlinked resources: Resource and Resources. The Resource type represents a single resource, whereas Resources is a collection of resources. When returned from a REST controller method, the links will be included in the JSON received by the client.

The original controller:

1
2
3
4
5
@GetMapping("/recent")
public Iterable<Taco> recentTacos() {
PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());
return tacoRepo.findAll(page).getContent();
}

The original implementation returned a List<Taco> , which was fine at the time. But we need to return a Resources object instead. Below is the first step:

1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/recent")
public Resources<Resource<Taco>> recentTacos() {

PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());
List<Taco> tacos = tacoRepo.findAll(page).getContent();

Resources<Resource<Taco>> recentResources = Resources.wrap(tacos);
recentResources.add(new Link("http://localhost:8080/design/recent", "recents"));

return recentResources;
}

In this new version of recentTacos() , we no longer return the list of tacos directly. Instead, we use Resources.wrap() to wrap the list of tacos as an instance of Resources<Resource<Taco>> , which is ultimately returned from the method.

Before returning the Resources object, we add a link whose relationship name is recents, and whose URL is http://localhost:8080/design/recent. The following JSON is included in the resource returned from the API request:

1
2
3
4
5
"_links": {
"recents": {
"href": "http://localhost:8080/design/recent"
}
}

At this point, the only added link is to the entire list; No links are added to the taco resources themselves, or to the ingredients of each taco. But first, we need to get rid of the hardcoded localhost:8080.

The Spring HATEOAS link builder provides ControllerLinkBuilder, and it knows what the hostname is without having to hardcode it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@GetMapping("/recent")
public Resources<Resource<Taco>> recentTacos() {

PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());
List<Taco> tacos = tacoRepo.findAll(page).getContent();
Resources<Resource<Taco>> recentResources = Resources.wrap(tacos);

// Rewrite begins: Below is the previous line
// recentResources.add(new Link("http://localhost:8080/design/recent", "recents"));
recentResources.add(ControllerLinkBuilder.linkTo(DesignTacoController.class)

// Note slash; withRel: relation name is recents
.slash("recent").withRel("recents"));

return recentResources;
}

Now we get rid of the hardcoded hostname, and we also don’t have to specify the /design path. Instead, we ask for a link to DesignTacoController, whose base path is /design. ControllerLinkBuilder uses the controller’s base path as the foundation of the Link object we’re creating.


Now we move one step further, and get rid of the /recent path as well. We can call linkTo() by giving it a method on the controller to have ControllerLinkBuilder derive the base URL from both the controller’s base path and the method’s mapped path:

1
2
3
4
5
6
7
8
9
10
11
12
13
@GetMapping("/recent")
public Resources<Resource<Taco>> recentTacos() {

PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());
List<Taco> tacos = tacoRepo.findAll(page).getContent();
Resources<Resource<Taco>> recentResources = Resources.wrap(tacos);

// Get rid of .slash
recentResources.add(linkTo(methodOn(DesignTacoController.class)
.recentTacos()).withRel("recents"));

return recentResources;
}

Now the entire URL is derived from the controller’s mappings. The methodOn() takes the controller class, and lets us make a call to the recentTacos() method, which is intercepted by ControllerLinkBuilder and used to determine both the controller’s base path and also the path mapped to recentTacos() .


Creating Resource Assemblers

Now we need to add links to the resource in the list.

One option is to loop through each of the Resource<Taco> elements in the Resources object, adding a Link to each individually. But we need to repeat the loop in the API wherever we return a list of taco resources.

Hence we’re going to define a utility class that converts Taco objects to a new TacoResource object. The new TacoResource object will be able to carry links.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// TacoResource: A Resource, Not a Domain!
// Extends: ResourceSupport
public class TacoResource extends ResourceSupport {

@Getter
private final String name;

@Getter
private final Date createdAt;

@Getter
private final List<Ingredient> ingredients;

public TacoResource(Taco taco) {
this.name = taco.getName();
this.createdAt = taco.getCreatedAt();
this.ingredients = taco.getIngredients();
}
}

Extends ResourceSupport: Inherit a list of Link objects and methods, to manage list of links.

Note: TacoResource does not include the id property from Taco. There’s no need to expose any database-specific IDs in the API. The resource’s self link will serve as the resource identifier.

Domain & Resource:

Create a separate resource type, so that Taco isn’t unnecessarily cluttered with resource links where links aren’t needed. Also, leave the id property out so it won’t be exposed to the API.

Create a resource assembler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Extend the superclass
public class TacoResourceAssembler extends ResourceAssemblerSupport<Taco, TacoResource> {

// Default constructor to inform the superclass
public TacoResourceAssembler() {
super(DesignTacoController.class, TacoResource.class);
}

@Override
protected TacoResource instantiateResource(Taco taco) {
return new TacoResource(taco);
}

@Override
public TacoResource toResource(Taco taco) {

// Get the ID
return createResourceWithId(taco.getId(), taco);
}
}

The default constructor: Informs the superclass ( ResourceAssemblerSupport ) that it will use DesignTacoController to determine the base path for any URLs in links it creates, when creating a TacoResource.

The instantiateResource() method is overridden: Given a Taco, instantiate a TacoResource. This method would be optional if TacoResource had a default constructor. In this case, TacoResource requires construction with a Taco , so we’re required to override it.

The toResource() method is the only method that’s strictly mandatory when extending ResourceAssemblerSupport. Here we create a TacoResource object from a Taco , and automatically give it a self link with the derived URL from the id property.

On the surface, toResource() seems similar to instantiateResource(), but they’re different. Under the covers, toResource() will call instantiateResource().instantiateResource() only instantiates a Resource object, while toResource() both creates the Resource object, and populates it with links.


Now, use TacoResourceAssembler in the controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@GetMapping("/recent")
public Resources<Resource<Taco>> recentTacos() {

PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());
List<Taco> tacos = tacoRepo.findAll(page).getContent();

// Rewrite this line below: Use Resource Assembler
// Resources<Resource<Taco>> recentResources = Resources.wrap(tacos);

List<TacoResource> tacoResources = new TacoResourceAssembler().toResources(tacos);
Resources<TacoResource> recentResources = new Resources<TacoResource>(tacoResources);

recentResources.add(linkTo(methodOn(DesignTacoController.class)
.recentTacos()).withRel("recents"));

return recentResources;
}

Rather than return Resources<Resource<Taco>>, recentTacos() now returns Resources<TacoResource> to take advantage of the new TacoResource type.

After fetching the tacos from the repository, we pass the list of Taco objects to the toResources() method on a TacoResourceAssembler, which will cycle through all Taco objects, calling the overridden toResource method to create a list of TacoResource objects.

With that TacoResource list, we then create a Resources<TacoResource> object, and populate it with the recents links.


At this point, a GET request to /design/recent will produce a list of tacos, each with a self link and a recents link on the list itself. But the ingredients will still be without a link. Therefore, we first create the IngredientResource object:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class IngredientResource extends ResourceSupport {

@Getter
private String name;

@Getter
private Type type;

public IngredientResource(Ingredient ingredient) {
this.name = ingredient.getName();
this.type = ingredient.getType();
}
}

Then create a new resource assembler for ingredients:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class IngredientResourceAssembler extends ResourceAssemblerSupport<Ingredient, IngredientResource> {

public IngredientResourceAssembler() {
super(IngredientController.class, IngredientResource.class);
}

@Override
public IngredientResource toResource(Ingredient ingredient) {
return createResourceWithId(ingredient.getId(), ingredient);
}

@Override
protected IngredientResource instantiateResource(Ingredient ingredient) {
return new IngredientResource(ingredient);
}
}

Slightly change TacoResource, so it carries IngredientResource objects instead of Ingredient objects:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TacoResource extends ResourceSupport {

// Uses toResource() to convert a Taco object’s Ingredient list to IngredientResource list
// Add this
private static final IngredientResourceAssembler ingredientAssembler = new IngredientResourceAssembler();

@Getter
private final String name;

@Getter
private final Date createdAt;

// Modify this
@Getter
private final List<IngredientResource> ingredients;

public TacoResource(Taco taco) {
this.name = taco.getName();
this.createdAt = taco.getCreatedAt();

// Modify this
this.ingredients = ingredientAssembler.toResources(taco.getIngredients());
}
}

Now the recent tacos list is completely outfitted with hyperlinks, not only for the recents link, but also for all of its taco entries and the ingredients of those tacos. The response should look a lot like the JSON in here.


Naming Embedded Relations

If we take a closer look at the JSON in here. we notice that the top-level elements look like this:

1
2
3
4
{
"_embedded": {
"tacoResourceList": [
{ ...

The name tacoResourceListwas based on creating Resources object from List<TacoResources>. If we were to refactor the
name of the TacoResource class to something else, the field name in the resulting JSON would change to match it. This would likely break the clients code that were counted on that name.

To tackle this problem, we use @Relation to help break the coupling between JSON field name and resource type class names.

1
2
@Relation(value="taco", collectionRelation="tacos")
public class TacoResource extends ResourceSupport { ... }

Here we’ve specified that when a list of TacoResource objects is used in a Resources object, it should be named tacos. And a single TacoResource object should be referred to in JSON as taco . Now the returned JSON looks like this:

1
2
3
4
{
"_embedded": {
"tacos": [
{ ...

Controller Evolution

The evolution of a controller method with HATEOAS:

Return list of tacos directly.

1
2
3
4
5
6
7
// Get recent designs, with pagination

@GetMapping("/recent")
public Iterable<Taco> recentTacos() {
PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());
return tacoRepo.findAll(page).getContent();
}

Wrap the list, but still has hardcoded URL.

1
2
3
4
5
6
7
8
9
10
11
12
13
@GetMapping("/recent")
public Resources<Resource<Taco>> recentTacos() {

PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());
List<Taco> tacos = tacoRepo.findAll(page).getContent();

Resources<Resource<Taco>> recentResources = Resources.wrap(tacos);

// Still hardcoded URL
recentResources.add(new Link("http://localhost:8080/design/recent", "recents"));

return recentResources;
}

Partially get rid of the hardcoded URL.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@GetMapping("/recent")
public Resources<Resource<Taco>> recentTacos() {

PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());
List<Taco> tacos = tacoRepo.findAll(page).getContent();

Resources<Resource<Taco>> recentResources = Resources.wrap(tacos);

// Rewrite: hostname & /design
recentResources.add(ControllerLinkBuilder.linkTo(DesignTacoController.class)
.slash("recent").withRel("recents"));

return recentResources;
}

Get rid of all hardcoded URL.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@GetMapping("/recent")
public Resources<Resource<Taco>> recentTacos() {

PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());
List<Taco> tacos = tacoRepo.findAll(page).getContent();

Resources<Resource<Taco>> recentResources = Resources.wrap(tacos);

// Rewrite: /recent, use static method of link builder
recentResources.add(linkTo(methodOn(DesignTacoController.class)
.recentTacos()).withRel("recents"));

return recentResources;
}

Add Resource Assembler.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@GetMapping("/recent")
public Resources<Resource<Taco>> recentTacos() {

PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());
List<Taco> tacos = tacoRepo.findAll(page).getContent();

// Rewrite: Use Resource Assembler
List<TacoResource> tacoResources = new TacoResourceAssembler().toResources(tacos);
Resources<TacoResource> recentResources = new Resources<TacoResource>(tacoResources);

recentResources.add(linkTo(methodOn(DesignTacoController.class)
.recentTacos()).withRel("recents"));

return recentResources;
}


Spring Data REST

Spring HATEOAS makes adding links to your API rather straightforward and simple, but it did add several lines of code that we otherwise do not need. However, if we want to be lazy, then let’s see how Spring Data REST can automatically create APIs based on the data repositories we’ve created.

With Spring Data REST, there’s no need to create Rest Controllers.

Spring Data REST is another member of the Spring Data family that automatically creates REST APIs for repositories created by Spring Data. By only adding Spring Data REST to the build, we get an exposed REST API with operations for each repository interface we’ve defined.

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>

We need to set a base path for the API, so the endpoints are all distinct:

1
spring.data.rest.basepath: /api

And make requests such as:

1
2
curl localhost:8080/api
curl localhost:8080/api/orders

Adjust Resource Paths & Relation Names

When creating endpoints for Spring Data repositories, Spring Data REST will pluralize the associated entity class. For instance, /users for User Entity, /tacoes for Taco Entity (tacoes, not tacos).

However, the correct spelling should be tacos. Hence we modify the Taco Entity class by adding the following annotation:

1
2
@RestResource(rel="tacos", path="tacos")
public class Taco { ... }

Setting this would give the result:

1
2
3
4
"tacos" : {
"href" : "http://localhost:8080/api/tacos{?page,size,sort}",
"templated" : true
},

Paging & Sorting

We noticed that the link above contains paths to page, size, and sort. By default, requests to a collection resource such as /api/tacos will return 20 items per page. But we can customize it:

1
2
# 5 items per page, only dispay the 2nd page(页数从0算起)
curl localhost:8080/api/tacos?size=5&page=1

And HATEOAS will offer links for the first, last, next, previous pages in the response.


The sort parameter lets us sort the result. For instance, we need to fetch the 12 most recently created tacos:

1
curl localhost:8080/api/tacos?sort=createdAt,desc&page=0&size=12

However the only problem left is that, the UI client still uses hardcoded request. So it’s better to make the client look up the URL from a list of links, hence we need to add custom endpoints & links.


When we write our own API controllers along with the ones provided by Spring Data REST, there are 2 problems:

  • Our own controller endpoints aren’t mapped under Spring Data REST’s base path. We can force mappings to be prefixed with the base path. But if the base path changes, we need to also change the base path in the controllers.
  • Any endpoints defined in the controllers won’t be automatically included as hyperlinks. This means that clients won’t be able to discover the custom endpoints with a relation name.

We first solve the problem of the base path. Spring Data REST includes a new controller called @RepositoryRestController. It annotates the controller class, and all mapping will have the same base path as in Spring Data REST.

Then we dump the old DesignTacoController and create the new controller that only contains recentTacos() method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Make sure the path will be: /api/tacos/recent
@RepositoryRestController
public class RecentTacosController {

private TacoRepository tacoRepo;

public RecentTacosController(TacoRepository tacoRepo) {
this.tacoRepo = tacoRepo;
}

// produces: hal & json
@GetMapping(path="/tacos/recent", produces="application/hal+json")

// Add Response Entity
// Or: @ResponseBody
public ResponseEntity<Resources<TacoResource>> recentTacos() {

// 和之前一样
PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());
List<Taco> tacos = tacoRepo.findAll(page).getContent();

List<TacoResource> tacoResources = new TacoResourceAssembler().toResources(tacos);
Resources<TacoResource> recentResources = new Resources<TacoResource>(tacoResources);

recentResources.add(linkTo(methodOn(RecentTacosController.class)
.recentTacos()).withRel("recents"));

// 就这一行不一样,因为 Add Response Entity
return new ResponseEntity<>(recentResources, HttpStatus.OK);
}
}

Note:

@RepositoryRestController doesn’t assures that returned values are written to the response body, hence we need to either add ResponseEntity, or use the class-level @ResponseBody.


Now we deal with the hyperlinks when clients search for relations, to make the custom endpoints appear in the hyperlinks list when client requests /api/tacos. In this case, we need a resource processor bean.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class SpringDataRestConfiguration {

// Declared as bean to be created in the spring application context
@Bean
public ResourceProcessor<PagedResources<Resource<Taco>>> tacoProcessor(EntityLinks links) {

// Defined as anonymous inner class
return new ResourceProcessor<PagedResources<Resource<Taco>>>() {

@Override
public PagedResources<Resource<Taco>> process(PagedResources<Resource<Taco>> resource) {
resource.add(links.linkFor(Taco.class).slash("recent").withRel("recents"));
return resource;
}
};
}
}

Spring HATEOAS will auto discover the ResrouceProcessor Bean(s), and will apply them to the appropriate resources.

In this case, if a PagedResources<Resource<Taco>> is returned from a controller, it will receive a link for the most recently created tacos. This includes the response from requests for /api/tacos.