领先一步
VMware 提供培训和认证,以加速您的进步。
了解更多Spring 社区成员们,您好!
正如之前宣布的那样,我们已在 1.0.0.BUILD-SNAPSHOT 中发布了新的 **Affordances API**。在这篇博文中,我们将深入了解此功能的具体用途。
首先,什么是 **affordance**?通过一些考据,REST 倡导者 Mike Admundsen 有一篇文章详细介绍了这个词的起源,可以追溯到至少 1986 年
环境的 affordances 是它提供的……它提供的或提供的,无论好坏。动词“to 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 提供的功能是将一个端点与另一个端点连接起来。例如,您可以将单个项目资源的 **GET** 端点(/employees/{id}
)链接到用于更新该员工的 **PUT** 映射(/employees/{id}
)。以下代码显示了这种关系
@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" : "https://127.0.0.1:8080/employees/1"
},
"employees" : {
"href" : "https://127.0.0.1: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="https://127.0.0.1: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 的反馈。
干杯!