可以看到,上述响应结果包含了链接地址,因此使用该API的客户端就能够自由选择要下载的具体图片。这些链接告知了客户端有哪些可供选择的图片,以及它们的地址在哪里。这样一来,客户端就能够根据不同的场景,做出符合自身需要的选择。而且,如果客户端只需要一种格式的图片,那就无须下载全部三种版本的图片。因此,这种表现形式有很多优势超级链接命令可实现,既减少了网络负载,又提高了客户端的灵活性,更增进了API的可探索性。
HATEOAS的重要性在于打破了客户端和服务器之间严格的契约,使得客户端可以更加智能和自适应,而RESTful服务本身的演化和更新也变得更加容易。我们知道在使用普通的RESTful API时,客户端需要根据服务器提供的相关文档来了解所暴露的资源和对应的操作。而基于HATEOAS的RESTful API可以实现服务端和客户端的解耦,客户端通过服务器提供的资源来智能地发现可执行操作,这就是所谓的自解释Web API。
2. HAL
HATEOAS更多是一种概念,而HAL(Hypertext Application Language,超文本应用语言)是HATEOAS的一种实现方式。与普通的RESTful风格不同,对每个资源,HAL又将其细分成状态(State)、链接(Links)和子资源(Embedded Resource)三个标准部分,如图4-4所示。
图4-4 HAL模型结构
这里的资源状态是指资源本身固有的属性,链接定义了与当前资源相关的一组资源的链接集合,而子资源则描述当前资源的具体内容,提供嵌套资源的定义。
举例来说,在不使用HAL的场景下,我们设计一个RESTful风格的API一般会采用如代码清单4-43所示的表现形式。
代码清单4-43RESTful风格请求和响应示例代码
//请求
GET
//响应Content-Type: application/json
"id": "user1",
"name": "tianyalan",
"email": "tianyalan@email.com"
而为了让API返回的数据更具有关联性,我们使用HAL+JSON格式,这时候返回的格式就会变成如代码清单4-44所示的表现形式。注意到这里多了_links属性,其中有一个self.href链接指向当前user资源。
代码清单4-44HAL+JSON风格请求和响应示例代码
//请求
GET
//响应
Content-Type: application/json
_links: {
self: {
href: "/users/tianyalan"
"id": "user1",
"name": "tianyalan",
"email": "tianyalan@email.com"
HAL的出现主要弥补了普通JSON格式在API交互中的不足,让JSON更具有自描述性和导航性。同时,我们也注意到Web应用程序是将API组合起来为系统提供服务的开发模式。在组合API时,JSON在格式上缺乏描述性的缺陷就体现得非常明显。我们要为API编写文档,要为API之间的数据关系和交互方式提供详细的说明。而如果使用HAL+JSON来描述如代码清单4-45所示的一个带有location属性的user信息,我们就可以很清晰地知道user信息和location信息的来源,这些信息都在子资源中有所体现。
代码清单4-45带有location属性的HAL+JSON风格请求和响应示例代码
//请求
GET
//响应
Content-Type: application/json
_links: {
self: {
href: "/users/tianyalan"
"id": "user1",
"name": "tianyalan",
"email": "tianyalan@email.com"
_embedded: {
location: {
_links: {
self: {
href: ' api.locationservices.com/locations/1'
},
id: 1,
city: 'hangzhou'
讲到这里,你可能会好奇,我们应该使用什么框架来开发类似这样的WebAPI呢?Spring HATEOAS就是这样一个功能强大的开发框架。事实上,SpringBoot框架内部也大量使用了Spring HATEOAS来对外暴露自解释的Web API。
引入Spring HATEOAS
Spring HATEOAS为Spring带来了超媒体支持。它提供了一组类和资源装配器(Assembler),当资源从Spring WebMVC控制器返回时,可以实现在这些资源之前添加对应的链接。想要在Spring Boot应用程序中启用超媒体,需要将如代码清单4-46所示的HATEOAS依赖项添加到项目中。
代码清单4-46spring-boot-starter-hateoas依赖包定义代码
org.springframework.boot
spring-boot-starter-hateoas
对于Spring HATEOAS而言,它试图解决的核心问题是链接的创建和表示的组装。在1.0版本之前,Spring HATEOAS提供了两种代表超链接的主要类型,即Resource和Resources,它们都是ResourceSupport的子类。其中,Resource表示单个资源,而Resources是资源的集合。这两种类型都能够承载指向其他资源的链接。
而在1.0版本之后,Spring HATEOAS中的模型发生了巨大的调整。
ResourceSupport、Resource和Resources分别被RepresentationModel、EntityModel和CollectionModel对象取代。
Spring HATEOAS案例分析
现在,让我们从一个实战案例切入,来演示如何使用Spring HATEOAS实现自解释Web API的开发步骤。我们先来设计一个实体类,如代码清单4-47所示。
代码清单4-47Employee类定义代码
public class Employee {
private final int id;
private String firstName;
private String lastName;
private String role;
//省略构造函数和getter/setter
为了简化演示过程,我们直接构建一个Service层组件EmployeeService超级链接命令可实现,该组件内部使用一个数组来进行内存级别的数据管理,基本就是对Employee对象的CRUD,如代码清单4-48所示。
代码清单4-48EmployeeService的CRUD操作代码
@Service
public class EmployeeService {
private static final List EMPLOYEES = new ArrayList();
private EmployeeService() {
create(new Employee("FirstName1", "LastName1", "USER"));
create(new Employee("FirstName2", "LastName2", "ADMIN"));
public List findAll() {
return EMPLOYEES;
public Employee findById(int id) {
return EMPLOYEES.get(id);
} public Employee findByName(String firstName, String lastName) {
return EMPLOYEES.stream().filter(employee ->
employee.getFirstName().equals(firstName) &&
employee.getLastName().equals(lastName)).findFirst().orElseThrow(() ->
EmployeeNotFound.byName(firstName + " " + lastName));
public Employee findByRole(String role) {
return EMPLOYEES.stream().filter(employee ->
employee.getRole().equals(role)).findFirst().orElseThrow(() ->
EmployeeNotFound.byRole(role));
public Employee create(Employee newEmployee) {
Employee newlyCreatedEmployee = new Employee(EMPLOYEES.size(),
newEmployee.getFirstName(), newEmployee.getLastName(),
newEmployee.getRole());
EMPLOYEES.add(newlyCreatedEmployee);
return newlyCreatedEmployee;
public Employee replace(Employee updatedEmployee, int id) {
EMPLOYEES.remove(id);
EMPLOYEES.add(id, updatedEmployee);
return findById(id);
定义了领域实体以及Service层组件之后,接下来就可以对资源和链接进行有效的管理。
1. 创建资源和链接
我们先来看一个查询单个Employee的示例。如果使用传统的RESTful风格,我们可以创建一个PlainController,然后实现如代码清单4-49所示的一组HTTP端点。
代码清单4-49PlainController类代码
@RestController
public class PlainController {
private final EmployeeService employeeService;
public PlainController(EmployeeService employeeService) {
this.employeeService = employeeService;
@GetMapping("/plain/employees")
public List all() {
return this.employeeService.findAll();
@PostMapping("/plain/employees")
public Employee create(@RequestBody Employee newEmployee) {
return this.employeeService.create(newEmployee);
@GetMapping("/plain/employees/{id}")
public Employee single(@PathVariable int id) {
return this.employeeService.findById(id);
@PutMapping("/plain/employees/{id}")
public Employee update(@RequestBody Employee updatedEmployee,
@PathVariable int id) {
return this.employeeService.replace(updatedEmployee, id);
可以看到,在未引入Spring HATEOAS时,HTTP端点返回的就是一个Employee对象。现在,我们创建一个HypermediaController,并尝试对PlainController中的single()方法进行重构,重构之后的结果如代码清单4-50所示。
代码清单4-50HypermediaController类代码
@RestController
public class HypermediaController {
private final EmployeeService employeeService;
@GetMapping("/hypermedia/employees/{id}")
public EntityModel single(@PathVariable int id) {
Link selfLink =
linkTo(methodOn(HypermediaController.class).single(id)).withSelfRel();
Affordance update =
afford(methodOn(HypermediaController.class).update(null, id));
Link aggregateRoot =
linkTo(methodOn(HypermediaController.class).all()).withRel("employees"
);
return EntityModel.of(employeeService.findById(id),
selfLink.andAffordance(update), aggregateRoot);
首先注意到上述single()方法的返回值是一个EntityModel对象。我们可以通过如代码清单4-51所示的方法来构建一个EntityModel对象。
代码清单4-51创建EntityModel示例代码
Employee employee = new Employee...
EntityModel model = EntityModel.of(employee);
当然,如果你想构建包含多个业务对象的CollectionModel,也可以采用类似的实现方式,如代码清单4-52所示。
代码清单4-52创建CollectionModel示例代码
Collection employees = Collections...;
CollectionModel model = CollectionModel.of(employees);
另外,single()方法包含了一组创建超媒体资源常见的工具方法,其中methodOn()方法相当于为Controller创建了一个代理类,该代理类记录Controller中指定方法的调用。通过methodOn()方法,我们知道需要为哪个方法创建链接,正如代码清单4-50中methodOn(HypermediaController.class). single(id)这行代码的作用对象是HypermediaController中的single()方法。
这里的linkTo()方法比较好理解,就是对methodOn()指定的目标方法创建一个链接。而withRel()方法用于定义链接关系的名称。例如在代码清单4-50中,我们将Hypermedia-Controller中的另一个all()方法命名为employees。对应地,withSelfRel()则使用默认的自链接(Self Link)关系为当前方法指定一个链接名称。
注意,这里还存在一个Affordance对象,Affordance的字面意思就是“功能可见性”。换句话说,我们可以通过Affordances来展示Controller中的其他功能。在代码清单4-50中,我们通过afford(methodOn(HypermediaController.class).update(null, id))语句告诉客户端在HypermediaController中存在一个update()方法,这里的afford()方法会自动获取该方法的HTTP请求方式以及请求参数,从而为客户端提供调用该方法的有效途径。
我们运行Spring Boot应用程序,并通过GET方法访问:8080/hypermedia/employees/{id}端点,得到的结果如代码清单4-53所示。
版权声明
本文仅代表作者观点。
本文系作者授权发表,未经许可,不得转载。
发表评论