在RESTful API设计中应用HATEOAS

在RESTful API设计中应用HATEOAS

HATEOAS 是 REST(Representational state transfer) 的约束之一。

HATEOAS 是 Hypermedia As The Engine Of Application State 的缩写,从字面上理解是 “超媒体即是应用状态引擎” 。其原则就是客户端与服务器的交互完全由超媒体动态提供,客户端无需事先了解如何与数据或者服务器交互。相反的,在一些RPC服务或者Redis,Mysql等软件,需要事先了解接口定义或者特定的交互语法。

HATEOAS在REST中的地位 #

在Richardson Maturity Model模型中,将RESTful分为四步,其中第四步 Hypermedia Controls 也就是HATEOAS。

  • 第一个层次(Level 0)的 Web 服务只是使用 HTTP 作为传输方式,实际上只是远程方法调用(RPC)的一种具体形式。SOAP 和 XML-RPC 都属于此类。
  • 第二个层次(Level 1)的 Web 服务引入了资源的概念。每个资源有对应的标识符和表达。
  • 第三个层次(Level 2)的 Web 服务使用不同的 HTTP 方法来进行不同的操作,并且使用 HTTP 状态码来表示不同的结果。如 HTTP GET 方法来获取资源,HTTP DELETE 方法来删除资源。
  • 第四个层次(Level 3)的 Web 服务使用 HATEOAS。在资源的表达中包含了链接信息。客户端可以根据链接来发现可以执行的动作。

REST的设计者Roy T. Fielding在博客 REST APIs must be hypertext-driven 中的几点原则强调非HATEOAS的系统不能称为RESTful。

  1. REST API决不能定义固定的资源名称或者层次关系。
  2. 使用REST API应该只需要知道初始URI(书签)和一系列针对目标用户的标准媒体类型。

HATEOAS 例子 #

通过实现HATEOAS,每个资源能够描述针对自己的操作资源,动态的控制客户端,即便更改了URL也不会破坏客户端。

下面是个例子,首先是一个GET请求

1    GET /account/12345 HTTP/1.1
2    Host: somebank.org
3    Accept: application/xml
4    ...

将会返回

 1   HTTP/1.1 200 OK
 2   Content-Type: application/xml
 3   Content-Length: ...
 4
 5   <?xml version="1.0"?>
 6   <account>
 7      <account_number>12345</account_number>
 8      <balance currency="usd">100.00</balance>
 9      <link rel="deposit" href="https://somebank.org/account/12345/deposit" />
10      <link rel="withdraw" href="https://somebank.org/account/12345/withdraw" />
11      <link rel="transfer" href="https://somebank.org/account/12345/transfer" />
12      <link rel="close" href="https://somebank.org/account/12345/close" />
13    </account>

返回的body不仅包含了账号信息:账户编号:12345,账号余额100,同时还有四个可执行链接分别可以执行deposit,withdraw,transferclose

一段时间后再次查询用户信息时返回

 1   HTTP/1.1 200 OK
 2   Content-Type: application/xml
 3   Content-Length: ...
 4
 5   <?xml version="1.0"?>
 6   <account>
 7       <account_number>12345</account_number>
 8       <balance currency="usd">-25.00</balance>
 9       <link rel="deposit" href="https://somebank.org/account/12345/deposit" />
10   </account>

这时用户账号余额产生赤字,可操作链接只剩一个deposit, 其余三个在赤字情况下无法执行。

HATEOAS的好处 #

让API变的可读性更高 ,实现客户端与服务端的部分解耦。对于不使用 HATEOAS 的 REST 服务,客户端和服务器的实现之间是紧密耦合的。客户端需要根据服务器提供的相关文档来了解所暴露的资源和对应的操作。当服务器发生了变化时,如修改了资源的 URI,客户端也需要进行相应的修改。而使用 HATEOAS 的 REST 服务中,客户端可以通过服务器提供的资源的表达来智能地发现可以执行的操作。当服务器发生了变化时,客户端并不需要做出修改,因为资源的 URI 和其他信息都是动态发现的。

Spring对HATEOAS的支持 #

Spring-HATEOAS允许我们创建遵循HATEOAS原则的API,显示给定资源的相关链接。

HATEOAS原则指出,API应该通过返回每个服务响应可能的后续步骤的信息来为客户提供指导。

使用Spring-HATEOAS,可以快速创建指向资源表示模型的链接的模型类。

以下是一个在Spring Boot应用中快速使用Spring-HATEOAS的例子:

 1    @RequestMapping(value = "/{licenseId}", method = RequestMethod.GET)
 2    public ResponseEntity<License> getLicense(@PathVariable("organizationId")   String organizationId,
 3                                              @PathVariable("licenseId") String licenseId) {
 4        License license = licenseService.getLicense(licenseId, organizationId);
 5        // HATEOAS的接口
 6        license.add(
 7            linkTo(methodOn(LicenseController.class).getLicense(organizationId, license.getLicenseId())).withSelfRel(),
 8            linkTo(methodOn(LicenseController.class).createLicense(organizationId, license, null)).withRel("createLicense"),
 9            linkTo(methodOn(LicenseController.class).updateLicense(organizationId, license)).withRel("updateLicense"),
10            linkTo(methodOn(LicenseController.class).deleteLicense(organizationId, license.getLicenseId())).withRel("deleteLicense")
11        );
12
13        return ResponseEntity.ok(license);
14    }
15
16    public class License extends RepresentationModel<License> { }

其中的模型类License继承了org.springframework.hateoas.RepresentationModel类,用来为模型类提供添加链接的能力。

请求上述接口,能够得到这样的返回:

 1{
 2    "id": 814,
 3    "licenseId": "license0x001",
 4    "description": "Software product",
 5    "organizationId": "123456",
 6    "productName": "Ostock",
 7    "licenseType": "full",
 8    "_links": {
 9        "self": {
10            "href": "http://localhost:8080/v1/organization/123456/license/license0x001"
11        },
12        "createLicense": {
13            "href": "http://localhost:8080/v1/organization/123456/license"
14        },
15        "updateLicense": {
16            "href": "http://localhost:8080/v1/organization/123456/license"
17        },
18        "deleteLicense": {
19            "href": "http://localhost:8080/v1/organization/123456/license/license0x001"
20        }
21    }
22}

返回体中的_links部分,就是HATEOAS为模型添加的链接。

HAL文档 #

Spring-HATEOAS使用HAL(Hypertext Application Language)规范来返回模型链接。

因此,上述使用Spring-HATEOAS之后的返回文档的媒体类型(Content-Type)是application/hal+json,HAL文档的格式类似于:

 1   GET /orders/523 HTTP/1.1
 2   Host: example.org
 3   Accept: application/hal+json
 4
 5   HTTP/1.1 200 OK
 6   Content-Type: application/hal+json
 7
 8   {
 9     "_links": {
10       "self": { "href": "/orders/523" },
11       "warehouse": { "href": "/warehouse/56" },
12       "invoice": { "href": "/invoices/873" }
13     },
14     "currency": "USD",
15     "status": "shipped",
16     "total": 10.20
17   }

除了接口的必要返回属性外,HAL还有一些保留属性,如上面的_links就是保留属性。

属性_links是可选。它是一个对象,它包含了一个或多个属性,每个属性可以是对象或者数组。

_links的每个属性都一个超链接对象,这些对象包含了资源至URL,他们有下列几个属性:

属性数据类型描述
href–string必填项,它的内容可以是URL 或者URL模板。
templated–bool可选项,默认为false,如果href是URL模板则templated必须为true
type–string可选项,它用来表示资源类型
deprecation–string可选项,表示该对象会在未来废弃
name–string可选项, 可能当作第二个key,当需要选择拥有相同name的链接时
profile–string可选项,简要说明链接的内容
title–string可选项,用用户可读的语音来描述资源的主题
hreflang–string可选项,用来表明资源的语言

_embedded #

属性_embedded是可选的。它是一个对象,它包含了一个或多个属性,每个属性是可以对象或者数组。

以下是一个HAL文档的例子:

 1   GET /orders HTTP/1.1
 2   Host: example.org
 3   Accept: application/hal+json
 4
 5   HTTP/1.1 200 OK
 6   Content-Type: application/hal+json
 7
 8   {
 9     "_links": {
10       "self": { "href": "/orders" },
11       "next": { "href": "/orders?page=2" },
12       "find": { "href": "/orders{?id}", "templated": true }
13     },
14     "_embedded": {
15       "orders": [{
16           "_links": {
17             "self": { "href": "/orders/123" },
18             "basket": { "href": "/baskets/98712" },
19             "customer": { "href": "/customers/7809" }
20           },
21           "total": 30.00,
22           "currency": "USD",
23           "status": "shipped",
24         },{
25           "_links": {
26             "self": { "href": "/orders/124" },
27             "basket": { "href": "/baskets/97213" },
28             "customer": { "href": "/customers/12369" }
29           },
30           "total": 20.00,
31           "currency": "USD",
32           "status": "processing"
33       }]
34     },
35     "currentlyProcessing": 14,
36     "shippedToday": 20
37   }

参考 #

REST HATEOAS入门 rest apis must be hypertext driven Spring-HATEOAS docs