抢占先机
VMware 提供培训和认证,助您加速进步。
了解更多Spring 社区的朋友们,您好:
如之前所宣布的,我们已在 1.0.0.BUILD-SNAPSHOT 版本中发布了一个新的 Affordances API。在这篇博文中,我们将初步了解这个特性具体能做什么。
首先,什么是 affordance? 进行一番考究,REST 的倡导者 Mike Admundsen 写了一篇文章详细介绍了这个词的起源,至少可以追溯到 1986 年
环境的 affordances 是它提供的……它提供或布置的东西,无论是好的还是坏的。动词 'afford' 在字典中可以找到,但名词 'affordance' 则没有。我创造了这个词(第 126 页)。
— 视觉感知的生态学方法 (Gibson)
它随后出现在 1988 年的一篇心理学论文中
……术语 affordance 指的是事物的感知属性和实际属性,主要是那些决定事物可能如何使用的基本属性。(第 9 页)
— 日常事物的设计|心理学 (Norman)
最后,它出现在 Roy Fielding 于 2008 年关于超媒体的一次演讲中
当我说超文本时,我指的是信息和控件的同时呈现,使得信息成为用户获取选择和选取行动的 affordance(幻灯片 #50)。
— 关于 REST 的幻灯片演示 (Fielding)
在所有这些情境中,“affordance” 指的是周围环境提供的可用行动。在 REST 的语境中,这些是由超媒体详细描述的行动。
过去,当人们从 SOAP 及其基于行动的策略转向时,他们难以记录其 API,许多人并不知道 Roy Fielding 正是为了这个目的而将超媒体构建到 REST 中。通过将数据与控件一起包含,不仅可以查找相关数据,还可以使用数据,这是关键。
使用 HAL 文档,客户端会获得非常简单的 affordances。链接会显示,但没有关于它们的其他信息。关于你可以用这些链接做什么以及如何与它们交互,都没有详细说明。
你可以通过定义以下域对象来开始探索这个新的 API
@Data
@Entity
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
class Employee {
@Id @GeneratedValue
private Long id;
private String firstName;
private String lastName;
private String role;
/**
* Useful constructor when id is not yet known.
*/
Employee(String firstName, String lastName, String role) {
this.firstName = firstName;
this.lastName = lastName;
this.role = role;
}
}
这个域对象提供了一个非常简单的 POJO 供我们交互。要持久化这类对象,你需要定义一个相应的 Spring Data JPA 仓库
interface EmployeeRepository extends CrudRepository<Employee, Long> {
}
这个仓库将提供最简单的 CRUD 操作。
有了这些组件,舞台已经搭好。你现在可以开始定义 REST 操作及其相应的 affordances。首先,创建一个 Spring MVC REST 控制器,如下所示
@RestController
class EmployeeController {
private final EmployeeRepository repository;
EmployeeController(EmployeeRepository repository) {
this.repository = repository;
}
...
}
这个控制器包含一些关键特性
@RestController
表明所有映射会将结果直接写入响应体,而不是渲染视图模板。
EmployeeRepository
通过构造函数注入,确保状态一致。
Spring HATEOAS 已经具备了从 Spring MVC 端点构建链接的能力。这个 API 提供的能力是将一个端点与另一个端点连接起来。例如,你可以将用于单个项资源(/employees/{id}
)的 GET 端点链接到用于更新该员工(/employees/{id}
)的 PUT 映射。以下代码展示了这种关系
@RestController
class EmployeeController {
...
@GetMapping("/employees/{id}")
ResponseEntity<Resource<Employee>> findOne(@PathVariable long id) {
return repository.findById(id)
.map(employee -> new Resource<>(employee, getSingleItemLinks(employee.getId())))
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PutMapping("/employees/{id}")
ResponseEntity<?> updateEmployee(@RequestBody Employee employee, @PathVariable long id) {
employee.setId(id);
Employee updatedEmployee = repository.save(employee);
Resource<Employee> employeeResource = new Resource<>(updatedEmployee, getSingleItemLinks(updatedEmployee.getId()));
try {
return ResponseEntity
.created(new URI(employeeResource.getRequiredLink(Link.REL_SELF).getHref()))
.body(employeeResource);
} catch (URISyntaxException e) {
return ResponseEntity.badRequest().body("Unable to update " + employee);
}
}
...
private List<Link> getSingleItemLinks(long id) {
return Arrays.asList(linkTo(methodOn(EmployeeController.class).findOne(id)).withSelfRel()
.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, id)))
.andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(id))),
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"));
}
}
在这段代码的中间,有几处使用了 .andAffordance(afford(methodOn(…)))
。这里是一个给定的链接(findOne
的 self 链接)连接到同一 URI 上的相关链接(updateEmployee
)。
有了这个版本,Spring HATEOAS 现在可以以媒体类型中立的格式获取 Spring MVC 端点的信息,从而可以将其提供给用户。这引出了一个问题——你如何向用户展示这种链接关系?
HAL 本身不包含任何用于显示这些 affordances 的格式。如果在给定资源的同一 URI 下有多个链接,HAL 只会简单地显示其中一个。值得庆幸的是,HAL 在 REST 领域越来越流行,这促使衍生规范开始发展。
HAL-FORMS 是一个 HAL 扩展,它看起来像任何其他 HAL 文档,但多了一个字段:_templates。这个字段允许显示方法以及属性。
假设上面提到的单个项 Resource<Employee>
代码将一个 Spring MVC @GetMapping
端点连接到一个 @PutMapping
端点(并且你的数据库中加载了一些员工数据),Spring HATEOAS 将会生成如下所示的 HAL-FORMS 超媒体
{
"id" : 1,
"firstName" : "Frodo",
"lastName" : "Baggins",
"role" : "ring bearer",
"_links" : {
"self" : {
"href" : "http://localhost:8080/employees/1"
},
"employees" : {
"href" : "http://localhost:8080/employees"
}
},
"_templates" : {
"default" : {
"title" : null,
"method" : "put",
"contentType" : "",
"properties" : [ {
"name" : "firstName",
"required" : true
}, {
"name" : "id",
"required" : true
}, {
"name" : "lastName",
"required" : true
}, {
"name" : "role",
"required" : true
} ]
},
"deleteEmployee" : {
"title" : null,
"method" : "delete",
"contentType" : "",
"properties" : [ ]
}
}
}
当你发起 GET /employees/1
请求时,这个 HAL-FORMS 文档同时显示数据和链接。但更重要的是,它为你提供了一个编辑资源的模板(default
模板)。由于 HAL-FORMS 假定你正对 self 链接进行操作,你可以通过 PUT /employees/1
来进行更新。它期望的属性包括 firstName
, id
, lastName
, 和 role
。
这些超媒体控件还表明你可以发起 DELETE /employees/1
请求(deleteEmployee
模板)。不涉及任何属性。
乍一看,这可能看起来不太令人印象深刻,因为你已经可以在上面显示的数据中读到这些信息了。但这种格式赋予你编写少量前端 JavaScript 并将该模板转换为以下内容的能力
<form method="put" action="http://localhost:8080/employees/1">
<input type="text" id="firstName" name="firstName"/>
<input type="text" id="id" name="id" />
<input type="text" id="lastName" name="lastName" />
<input type="text" id="role" name="role" />
<input type="submit" value="Submit" />
</form>
通过将 self 链接与列出的属性混合,你可以创建一个完全由超媒体驱动的真实 HTML 表单。这通过让服务器将特定领域细节直接推送给网站用户,从而完成了 REST 的协同效应。无需将这些领域知识硬编码到客户端,从而降低了耦合度。相反,只需将超媒体的模板转换成一个表单即可。然后,当服务器端发生领域更新时,客户端可以轻松地适应。
简而言之,HAL-FORMS 旨在显示针对同一 URI 的其他可用行动。
读到这里,你是否问自己这样一个问题:“为什么不直接推送一个 HTML 表单,而不是一些 JSON 呢?”这是一个合理的问题。
Affordances API 完全是中立的,允许 Spring 团队开发除 HAL-FORMS 之外的其他媒体类型。一些正在开发中的包括 Uber、SIREN、Collection+JSON 以及一种 XHTML 形式。
本文中的代码和详细信息可在 https://github.com/spring-projects/spring-hateoas-examples 找到,特别是在 Affordances 部分下。
我们期待社区对该 API 的反馈。
致敬!