使用 Spring HATEOAS 构建更丰富的超媒体

工程 | Greg L. Turnquist | 2018年1月12日 | ...

Spring 社区成员们,您好!

正如之前宣布的那样,我们已在 1.0.0.BUILD-SNAPSHOT 中发布了新的 **Affordances API**。在这篇博文中,我们将深入了解此功能的具体用途。

Affordances 的历史

首先,什么是 **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。链接显示,但没有其他信息。您如何使用这些链接以及与它们交互需要哪些操作都没有详细说明。

Affordances 和 Spring HATEOAS

您可以通过定义以下域对象开始探索此新 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-FORMS

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 来进行更新。它期望的属性包括 firstNameidlastNamerole

这些超媒体控件还表明您可以发出 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 之外的其他媒体类型。一些正在开发的媒体类型包括 UberSIRENCollection+JSON 和一种 XHTML 格式。

更多示例

本文中发现的代码和详细信息可以在 https://github.com/spring-projects/spring-hateoas-examples 中找到,尤其是在 **Affordances** 部分。

我们期待社区对该 API 的反馈。

干杯!

获取 Spring 时事通讯

与 Spring 时事通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部