到目前为止,我们已经有了基于 Web 的服务,该服务可以处理涉及员工数据的核心操作。但这还不足以使应用变得 “RESTful”。
- 像 /employee/3 这样的漂亮 URL 并不是 REST;
- 仅仅使用
GET
、POST
等不是 REST; - 安排所有 CRUD 操作不是 REST。
实际上,到目前为止,我们更好地描述了 RPC
(远程过程调用
)。那是因为无法知道如何与该服务进行互动。如果我们今天发布了该内容,则还必须写文档或将开发人员的门户托管在所有详细信息的某个位置。
Roy Fielding 的这一声明可能进一步为 REST 和 RPC 之间的区别提供了线索:
人们对将任何基于 HTTP 的接口称为 REST API 的人数感到沮丧。今天的示例是 SocialSite REST API。那就是 RPC。它叫 RPC。有太多应该定 X 等级的耦合。
在超文本是一个约束的概念上,需要采取什么措施才能使 REST 体系结构风格清晰明了?换句话说,如果应用状态的引擎(以及 API)不是由超文本驱动的,则它不能是 RESTful 的,也不能是 REST API。
— Roy Fielding
https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
在我们的表示中不包含超媒体的副作用是客户端必须使用硬编码 URI 来导航 API。这导致了与电子商务在网络上兴起之前一样的脆弱性。这表明我们的 JSON 输出需要一点帮助。
介绍一下 Spring HATEOAS,这是一个 Spring 项目,旨在帮助我们编写超媒体驱动的输出。要将服务升级为 RESTful,请将其添加到我们的构建中:
将 Spring HATEOAS 添加到 pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
这个小小的库将为我们提供定义 RESTful 服务的构造,然后将其呈现为可接受的格式以供客户端使用。
任何 RESTful 服务的关键要素是添加到相关操作的链接。为了使我们的控制器更加 RESTful,请添加以下链接:
获取单项资源:
@GetMapping("/employees/{id}")
EntityModel<Employee> one(@PathVariable Long id) {
Employee employee = repository.findById(id)
.orElseThrow(() -> new EmployeeNotFoundException(id));
return new EntityModel<>(employee,
linkTo(methodOn(EmployeeController.class).one(id)).withSelfRel(),
linkTo(methodOn(EmployeeController.class).all()).withRel("employees"));
}
相关的导入语句:
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
这与我们以前所做的很相似,但是有一些变化:
- 该方法的返回类型已从
Employee
更改为EntityModel<Employee>
。EntityModel<T>
是 Spring HATEOAS 的通用容器,不仅包含数据,还包含链接的集合; linkTo(methodOn(EmployeeController.class).one(id)).withSelfRel()
要求 Spring HATEOAS 建立到EmployeeController
的one()
方法的链接,并将其标记为 self 链接;linkTo(methodOn(EmployeeController.class).all()).withRel("employees")
要求 Spring HATEOAS 构建到聚合根,all()
的链接,并将其称为 “employees”。
“建立链接” 是什么意思?Spring HATEOAS 的核心类型之一是 Link
。它包含一个 URI 和一个 rel(关系)。链接是赋予网络力量。在魏王出现之前,其他文档系统会呈现信息或链接,但是文档与数据的链接将网络缝合在一起。
Roy Fielding 鼓励用使 Web 成功的技术来构建 API,链接就是其中之一。
如果我们重启该应用并查询 Bilbo 的员工记录,我们将得到之前稍有不同的响应。
单个员工的 RESTful 展示:
{
"id": 1,
"name": "Bilbo Baggins",
"role": "burglar",
"_links": {
"self": {
"href": "http://localhost:8080/employees/1"
},
"employees": {
"href": "http://localhost:8080/employees"
}
}
}
该解压缩后的输出不仅显示我们先前看到的数据元素(id
、name
及 role
),而且还显示一个包含两个 URI 的 _links
条目。整个文档使用 HAL 格式化。
HAL 是一种轻量级的媒体类型,它不仅允许对数据进行编码,还允许对超媒体空间进行编码,从而向消费者警告他们可以浏览的 API 的其他部分。在这种情况下,存在 “self” 链接(类似于代码中的 this
语句)以及返回聚合根的链接。
为了使聚合根 ALSO 更加 RESTful,我们希望包含顶级链接,同时还包含以下任何 RESTful 组件。
获取聚合根资源:
@GetMapping("/employees")
CollectionModel<EntityModel<Employee>> all() {
List<EntityModel<Employee>> employees = repository.findAll().stream()
.map(employee -> new EntityModel<>(employee,
linkTo(methodOn(EmployeeController.class).one(employee.getId())).withSelfRel(),
linkTo(methodOn(EmployeeController.class).all()).withRel("employees")))
.collect(Collectors.toList());
return new CollectionModel<>(employees,
linkTo(methodOn(EmployeeController.class).all()).withSelfRel());
}
哇!该方法曾经只是 repository.findAll()
而已,却变得很大!让我们将其拆开。
CollectionModel<>
是另一个 Spring HATEOAS 容器,旨在封装集合。他还允许我们包含链接。不要让第一条语句漏掉。“封装集合” 是什么意思?员工收款?
不完全的。
由于我们在谈论 REST,因此它应该封装员工资源的集合。
这就是为什么要获取所有员工,然后将它们转换为 EntityModel<Employee>
对象列表的原因。(感谢 Java 8 Stream API!)
如果重新启动应用并获取聚合根,则可以看到它的外观。
员工资源集合的 RESTful 展示:
{
"_embedded": {
"employeeList": [
{
"id": 1,
"name": "Bilbo Baggins",
"role": "burglar",
"_links": {
"self": {
"href": "http://localhost:8080/employees/1"
},
"employees": {
"href": "http://localhost:8080/employees"
}
}
},
{
"id": 2,
"name": "Frodo Baggins",
"role": "thief",
"_links": {
"self": {
"href": "http://localhost:8080/employees/2"
},
"employees": {
"href": "http://localhost:8080/employees"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/employees"
}
}
}
对于该聚合根(它提供了一系列员工资源),存在一个顶级 “self” 链接。“collection” 列在 “_embedded” 部分的下面。这就是 HAL 表示集合的方式。
并且集合中的每个成员都有其信息以及相关链接。
添加所有这些链接的意义何在?随着时间的推移,它可以发展 REST 服务。在将来添加新链接时,可以维护现有链接。较新的客户端可以利用新链接,而旧客户端可以在旧链接上维持自己的位置。如果服务被重新定位和移动,这将特别有用。只要保持链接结构,客户端就可以查找并与事物进行交互。