6.3 启用以数据为中心的服务

正如在第 3 章中看到的,Spring Data 拥有一种特殊的魔力,它根据在代码中定义的接口自动创建存储库的实现。但是 Spring Data 还有另一个技巧,可以为应用程序定义 API。

Spring Data REST 是 Spring Data 家族中的另一个成员,它为 Spring Data 创建的存储库自动创建REST API。只需将 Spring Data REST 添加到构建中,就可以获得一个 API,其中包含所定义的每个存储库接口的操作。

要开始使用 Spring Data REST,需要在构建中添加以下依赖项:

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

信不信由你,这就是在一个已经将 Spring Data 用于自动存储库的项目中公开 REST API 所需要的全部内容。通过在构建中简单地使用 Spring Data REST starter,应用程序可以自动配置,从而为 Spring Data 创建的任何存储库(包括 Spring Data JPA、Spring Data Mongo 等)自动创建 REST API。

Spring Data REST 创建的 REST 端点至少与自己创建的端点一样好(甚至可能更好)。因此,在这一点上,可以做一些拆卸工作,并在继续之前删除到目前为止创建的任何 @RestController 注解的类。

要尝试 Spring Data REST 提供的端点,可以启动应用程序并开始查看一些 url。基于已经为 Taco Cloud 定义的存储库集,应该能够执行针对 Taco、Ingredient、Order 和 User 的 GET 请求。

例如,可以通过向 /ingredients 接口发出 GET 请求来获得所有 Ingredient 的列表。使用 curl,可能会得到这样的结果(经过删节,只显示第一个 Ingredient):

  1. $ curl localhost:8080/ingredients
  2. {
  3. "_embedded" : {
  4. "ingredients" : [
  5. {
  6. "name" : "Flour Tortilla",
  7. "type" : "WRAP",
  8. "_links" : {
  9. "self" : {"href" : "http://localhost:8080/ingredients/FLTO"},
  10. "ingredient" : {
  11. "href" : "http://localhost:8080/ingredients/FLTO"
  12. }
  13. }
  14. },
  15. ...
  16. ]
  17. },
  18. "_links" : {
  19. "self" : {
  20. "href" : "http://localhost:8080/ingredients"
  21. },
  22. "profile" : {
  23. "href" : "http://localhost:8080/profile/ingredients"
  24. }
  25. }
  26. }

哇!通过向构建中添加一个依赖项,不仅获得了 Ingredient 的端点,而且返回的资源也包含超链接!假装是这个 API 的客户端,也可以使用 curl 来跟踪特定入口的自链接:

  1. $ curl localhost:8080/ingredients/FLTO
  2. {
  3. "name" : "Flour Tortilla",
  4. "type" : "WRAP",
  5. "_links" : {
  6. "self" : {
  7. "href" : "http://localhost:8080/ingredients/FLTO"
  8. },
  9. "ingredient" : {
  10. "href" : "http://localhost:8080/ingredients/FLTO"
  11. }
  12. }
  13. }

为了避免过于分散注意力,在本书中我们不会浪费太多时间来深入研究 Spring Data REST 创建的每个端点和选项。但是应该知道,它还支持其创建的端点的 POST、PUT 和 DELETE 方法。没错:可以通过向 /ingredients 接口发送 POST 请求创建一个新的 Ingredient,然后通过向 /indegredient/FLTO 接口发送 DELETE 请求来从菜单上移除面粉玉米饼。

可能想要做的一件事是为 API 设置一个基本路径,这样它的端点是不同的,并且不会与编写的任何控制器发生冲突。(事实上,如果不删除先前创建的 IngredientController,它将干扰 Spring Data REST 提供的 /ingredients 端点。)要调整 API 的基本路径,请设置 spring.data.rest 基本路径属性:

  1. spring:
  2. data:
  3. rest:
  4. base-path: /api

这将设置 Spring Data REST 端点的基本路径为 /api。因此,Ingredient 端点现在是 /api/ingredients。现在,通过请求一个 tacos 列表来使用这个新的基本路径:

  1. $ curl http://localhost:8080/api/tacos
  2. {
  3. "timestamp": "2018-02-11T16:22:12.381+0000",
  4. "status": 404,
  5. "error": "Not Found",
  6. "message": "No message available",
  7. "path": "/api/tacos"
  8. }

噢?这并没有达到预期的效果。有一个 Ingredient 实体和一个 IngredintRepository 接口,其中 Spring Data REST 暴露 /api/ingredients 端点。因此,如果有一个 Taco 实体和一个 TacoRepository 接口,为什么 Spring Data REST 不能提供 /api/tacos 端点呢?

6.3.1 调整资源路径和关系名称

实际上,Spring Data REST 提供了处理 tacos 的端点。但是,尽管 Spring Data REST 非常智能,但它在暴露 tacos 端点方面的表现却稍微逊色一些。

在为 Spring Data 存储库创建端点时,Spring Data REST 尝试使关联多元化的实体类。对于 Ingredient 实体,端点是 /ingredients。对于 Order 和 User 实体,它是 /orders/users。到目前为止,一切顺利。

但有时,比如 “taco”,它会在一个字母上出错,这样复数形式就不太正确了。事实证明,Spring Data REST 将复数形式 “taco” 表示为 “tacoes”,因此,要想对 tacos 发出请求,你必须请求 /api/tacoes

  1. % curl localhost:8080/api/tacoes
  2. {
  3. "_embedded" : {
  4. "tacoes" : [ {
  5. "name" : "Carnivore",
  6. "createdAt" : "2018-02-11T17:01:32.999+0000",
  7. "_links" : {
  8. "self" : {
  9. "href" : "http://localhost:8080/api/tacoes/2"
  10. },
  11. "taco" : {
  12. "href" : "http://localhost:8080/api/tacoes/2"
  13. },
  14. "ingredients" : {
  15. "href" : "http://localhost:8080/api/tacoes/2/ingredients"
  16. }
  17. }
  18. }]
  19. },
  20. "page" : {
  21. "size" : 20,
  22. "totalElements" : 3,
  23. "totalPages" : 1,
  24. "number" : 0
  25. }
  26. }

你可能想知道我怎么知道 “taco” 会被误拼成 “tacoes”。事实证明,Spring Data REST 还公开了一个 home 资源,其中包含所有公开端点的链接。只需向 API 基础路径发出 GET 请求即可获得:

  1. $ curl localhost:8080/api
  2. {
  3. "_links" : {
  4. "orders" : {
  5. "href" : "http://localhost:8080/api/orders"
  6. },
  7. "ingredients" : {
  8. "href" : "http://localhost:8080/api/ingredients"
  9. },
  10. "tacoes" : {
  11. "href" : "http://localhost:8080/api/tacoes{?page,size,sort}",
  12. "templated" : true
  13. },
  14. "users" : {
  15. "href" : "http://localhost:8080/api/users"
  16. },
  17. "profile" : {
  18. "href" : "http://localhost:8080/api/profile"
  19. }
  20. }
  21. }

可以看到,home 资源显示了所有实体的链接。除了 tacoes 链接之外,一切看起来都很好,其中关系名称和 URL 都有 “taco” 的单数复数形式。

好消息是,不必接受 Spring Data REST 的这个小怪癖。通过向 Taco 类添加一个简单的注解,可以调整关系名称和路径:

  1. @Data
  2. @Entity
  3. @RestResource(rel="tacos", path="tacos")
  4. public class Taco {
  5. ...
  6. }

@RestResource 注解让你可以给定任何你想要的的名称和路径的关系,在这个例子中,把它们都设定为了 “tacos”。现在当请求 home 资源的时候,将会看到 tacos 链接正确的复数形式:

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

这还可以对端点的路径进行排序,这样就可以针对 /api/tacos 接口发起请求来使用 taco 资源了。

说到排序,让我们看看如何对 Spring Data REST 端点的结果进行排序。

6.3.2 分页和排序

你可能注意到了在 home 资源的链接中,全部都有 page、size 和 sort 参数。默认情况下,像是对 /api/tacos 这种集合资源请求的接口来说,将会从第一页返回每页 20 个数据项。但是可以根据请求的要求,通过指定特定的 page 和 size 参数来调整页面大小和哪一页。

举个例子,要请求 tacos 的页面大小为 5 的第一页,可以发起以下 GET 请求(使用 curl):

  1. $ curl "localhost:8080/api/tacos?size=5"

假设有多于 5 条 tacos 数据,可以通过添加 page 参数请求 tacos 数据的第二页:

  1. $ curl "localhost:8080/api/tacos?size5&page=1"

注意 page 参数是从 0 开始的,意思是请求第 1 页实际上是请求的第 2 页。(还会注意到许多 shell 命令行在请求中的 & 符号上出错,这就是为什么我在前面的 curl 命令中引用整个 URL 的原因。)

可以使用字符串操作将这些参数添加到 URL 中,但是 HATEOAS 提供了响应中第一个、最后一个、下一个和前一个页面的链接:

  1. "_links" : {
  2. "first" : {
  3. "href" : "http://localhost:8080/api/tacos?page=0&size=5"
  4. },
  5. "self" : {
  6. "href" : "http://localhost:8080/api/tacos"
  7. },
  8. "next" : {
  9. "href" : "http://localhost:8080/api/tacos?page=1&size=5"
  10. },
  11. "last" : {
  12. "href" : "http://localhost:8080/api/tacos?page=2&size=5"
  13. },
  14. "profile" : {
  15. "href" : "http://localhost:8080/api/profile/tacos"
  16. },
  17. "recents" : {
  18. "href" : "http://localhost:8080/api/tacos/recent"
  19. }
  20. }

有了这些链接,API 的客户端就不需要跟踪它所在的页面并将参数连接到 URL。相反,它必须知道如何根据这些页面导航链接的名称查找其中一个链接并跟踪它。

sort 参数允许根据实体的任何属性对结果列表进行排序。例如,需要一种方法来获取最近创建的 12个 tacos,以便 UI 显示,可以通过指定以下分页和排序参数组合来做到这一点:

  1. $ curl "localhost:8080/api/tacos?sort=createAt,desc?page=0&size=12"

这里,sort 参数指定了应该根据 createdDate 属性进行排序,并按降序排序(以便最新的 tacos 排在前面)。页面和大小参数的指定确定了应该在第一个页面上看到 12 个 tacos。

这正是 UI 为了显示最近创建的 tacos 所需要的。它与在本章前面的 DesignTacoController 中定义的 /design/recent 端点大致相同。

不过有个小问题,需要对 UI 代码进行硬编码,以请求包含这些参数的 tacos 列表。但是,通过使客户端对如何构造 API 请求了解得太多而增加了客户端的一些弱兼容性。如果客户端可以从链接列表中查找 URL,那就太好了。如果 URL 更简洁,就像以前的 /design/recent 端点一样,那就更好了。

6.3.3 添加用户端点

Spring Data REST 非常擅长创建针对 Spring Data 存储库执行 CRUD 操作的端点。但是有时需要脱离默认的 CRUD API,并创建一个能够解决核心问题的端点。

没有任何东西可以阻止你在 @RestController 注解的 bean 中实现任何想要的端点,来补充 Spring Data REST 自动生成的内容。实际上,可以重新使用本章前面的 DesignTacoController,它仍然可以与 Spring Data REST 提供的端点一起工作。

但是,当你编写自己的 API 控制器时,它们的端点似乎以以下两种方式与 Spring Data REST 端点分离:

  • 自己的控制器端点没有映射到 Spring Data REST 的基本路径下。可以强制它们的映射以任何想要的基本路径作为前缀,包括 Spring Data REST 基本路径,但是如果基本路径要更改,需要编辑控制器的映射来匹配。
  • 在自己的控制器中定义的任何端点都不会自动作为超链接包含在 Spring Data REST 端点返回的资源中。这意味着客户端将无法发现具有关系名称的自定义端点。

让我们首先解决关于基本路径的问题。Spring Data REST 包括 @RepositoryRestController,这是一个用于控制器类的新注解,其映射应该采用与为 Spring Data REST 端点配置的基本路径相同的基本路径。简单地说,@RepositoryRestController 注解的控制器中的所有映射的路径都将以 spring.data.rest.base-path 的值为前缀(已配置为 /api)。

将创建一个只包含 recentTacos() 方法的新控制器,而不是重新启用 DesignTacoController,它有几个不需要的处理程序方法。下一个程序清单中的 RecentTacosController 使用 @RepositoryRestController 进行注解,以采用 Spring Data REST 的基本路径进行其请求映射。程序清单 6.7 为控制器应用 Spring Data REST 基础路径

  1. package tacos.web.api;
  2. import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
  3. import java.util.List;
  4. import org.springframework.data.domain.PageRequest;
  5. import org.springframework.data.domain.Sort;
  6. import org.springframework.data.rest.webmvc.RepositoryRestController;
  7. import org.springframework.hateoas.Resources;
  8. import org.springframework.http.HttpStatus;
  9. import org.springframework.http.ResponseEntity;
  10. import org.springframework.web.bind.annotation.GetMapping;
  11. import tacos.Taco;
  12. import tacos.data.TacoRepository;
  13. @RepositoryRestController
  14. public class RecentTacosController {
  15. private TacoRepository tacoRepo;
  16. public RecentTacosController(TacoRepository tacoRepo) {
  17. this.tacoRepo = tacoRepo;
  18. }
  19. @GetMapping(path="/tacos/recent", produces="application/hal+json")
  20. public ResponseEntity<Resources<TacoResource>> recentTacos() {
  21. PageRequest page = PageRequest.of(
  22. 0, 12, Sort.by("createdAt").descending());
  23. List<Taco> tacos = tacoRepo.findAll(page).getContent();
  24. List<TacoResource> tacoResources =
  25. new TacoResourceAssembler().toResources(tacos);
  26. Resources<TacoResource> recentResources =
  27. new Resources<TacoResource>(tacoResources);
  28. recentResources.add(
  29. linkTo(methodOn(RecentTacosController.class).recentTacos())
  30. .withRel("recents"));
  31. return new ResponseEntity<>(recentResources, HttpStatus.OK);
  32. }
  33. }

尽管 @GetMapping 映射到路径 /tacos/recent,但是类级别的 @RepositoryRestController 注解将确保它以 Spring Data REST 的基本路径作为前缀。正如所配置的,recentTacos() 方法将处理 /api/tacos/recent 的 GET 请求。

需要注意的一件重要事情是,尽管 @RepositoryRestController 的名称与 @RestController 类似,但它的语义与 @RestController 不同。具体来说,它不确保从处理程序方法返回的值被自动写入响应体。因此,需要使用 @ResponseBody 对方法进行注解,或者返回一个包装响应数据的 ResponseEntity。

使用 RecentTacosController,对 /api/tacos/recent 的请求将返回最多 15 个最近创建的 tacos,而不需要在 URL 中对参数进行分页和排序。但是,当请求 /api/tacos 时,它仍然不会出现在超链接列表中。让我们解决这个问题。

6.3.4 向 Spring Data 端点添加用户超链接

如果最近的 tacos 端点不在 /api/tacos 返回的超链接中,客户端如何知道如何获取最近的 tacos?它要么猜测,要么使用分页和排序参数。无论哪种方式,它都将在客户端代码中硬编码,这并不理想。

不过,通过声明资源处理器 bean,可以将链接添加到 Spring Data REST 自动包含的链接列表中。Spring Data HATEOAS 提供了 ResourceProcessor,这是一个在通过 API 返回资源之前操作资源的接口。出于需要自动包含链接列表的目的,需要对 ResourceProcessor 进行实现,该实现将一个最近链接添加到类型为 PagedResources> 的任何资源(为 /api/tacos 端点返回的类型)。下一个程序清单显示了定义这样一个 ResourceProcessor 的 bean 方法声明。程序清单 6.8 向 Spring Data REST 端点添加用户链接

  1. @Bean
  2. public ResourceProcessor<PagedResources<Resource<Taco>>>
  3. tacoProcessor(EntityLinks links) {
  4. return new ResourceProcessor<PagedResources<Resource<Taco>>>() {
  5. @Override
  6. public PagedResources<Resource<Taco>> process(
  7. PagedResources<Resource<Taco>> resource) {
  8. resource.add(
  9. links.linkFor(Taco.class)
  10. .slash("recent")
  11. .withRel("recents"));
  12. return resource;
  13. }
  14. };
  15. }

程序清单 6.8 中显示的 ResourceProcessor 被定义为一个匿名内部类,并声明为一个将在 Spring 应用程序上下文中创建的 bean。Spring HATEOAS 将自动发现这个 bean(以及 ResourceProcessor 类型的任何其他 bean),并将它们应用于适当的资源。在这种情况下,如果从控制器返回 PagedResources>,它将收到最近创建的 Taco 的链接。这包括对 /api/tacos 请求的响应。