在当前程序设计语言层出不穷,技术架构五花八门的现在,系统间交互已经渗透到程序设计的方方面面。作为当前最成熟的API设计理论,RESTful API在当下得到了最广泛的应用。
REST(Representational State Transfer,表属性状态转移)的概念源自于Roy Thomas Fielding博士在2000年发表的论文《Architectural Styles and the Design of Network-based Software Architectures》,而我们常说的RESTful则是指REST风格的API设计,即该API的设计应用了REST的思想。也就是说,基于REST的思想所设计出的API即可称之为RESTful API。但是一般情况下,我们对RESTful API的定义则更加狭隘,通常情况下,我们所属的RESTful API则是使用HTTP通信协议来传递JSON格式数据的API。
本文中,我们主要讨论这种基于HTTP协议传递JSON格式数据的RESTful API设计的最佳实践。
1. 资源
在RESTful的世界中,一切皆为资源,就好像在面向对象的世界中,一切皆为对象一样。因此在设计RESTful API的第一步,就是对资源的抽象。这里需要我们将系统中的一切都抽象为资源,包括数据和行为。在定义了资源之后,再为资源附加上其所允许的操作,即完成了对RESTful API的定义。
1.1 资源
对数据的抽象相对比较直观,一般来讲,数据可以直接映射成一种资源,例如订单、存储阵列、虚拟机等。而对于行为则会显得稍微有点抽象,例如对文件的拷贝或者移动,也需要视作一种资源。在实际的API设计中,资源呈现为HTTP中的URI,通常使用资源的复数形式来定义这个URI。例如,“/hosts”表示主机这种资源,而“/hosts/1”则表示ID为“1”的主机实例。
对于不同资源间所存在的关系,通过子资源的方式呈现。例如,“/hosts/1/switches”表示主机1所连接的交换机资源,而“/hosts/1/switches/1”表示主机1所连接的交换机1,即主机1与交换机1相连。等价地,“/switches/1/hosts/1”为交换机1与主机1相连,其所表示的含义是相同的,但是其所处的视角不同,分别以主机和交换机的视角来描述这种情况。
而对于行为,例如主机上下电,可通过“/hosts/1/powers”来表示主机的电源情况。
1.2 操作
在抽象了资源之后,就需要定义对这些资源的操作,而对每种资源,最多只能应用6种操作,即创建、删除、全量修改、增量修改、查询单个资源、列出所有资源。这些操作一般通过HTTP的URI和请求方法共同表示。
以主机资源为例:
URI |
请求方法 |
功能 |
/hosts |
POST |
创建一个主机实例。 |
/hosts/{id} |
PUT |
全量修改指定ID的主机信息。 |
/hosts/{id} |
PATCH |
增量修改指定ID的主机信息。 |
/hosts/{id} |
DELETE |
删除指定ID的主机实例。 |
/hosts/{id} |
GET |
查询指定ID的主机信息。 |
/hosts |
GET |
列出符合条件的主机信息,通过URL的查询参数来指定查询条件。 |
以主机与交换机的关系为例:
URI |
请求方法 |
功能 |
/hosts/{hostId}/switches/{switchId} |
POST |
将指定ID的主机与指定ID的交换机连接。 |
/hosts/{hostId}/switches/{switchId} |
PUT |
全量修改指定ID主机与指定ID交换机的连接信息。 |
/hosts/{hostId}/switches/{switchId} |
PATCH |
增量修改指定ID主机与指定ID交换机的连接信息。 |
/hosts/{hostId}/switches/{switchId} |
DELETE |
断开指定ID的主机与指定ID的交换机的连接。 |
/hosts/{hostId}/switches/{switchId} |
GET |
查询指定ID的主机与指定ID的交换机的连接信息。 |
/hosts/{hostId}/switches |
GET |
列出指定主机所连接的交换机的连接信息,通过URL查询参数指定查询条件。 |
以主机的电源为例:
URI |
请求方法 |
功能 |
/hosts/{id}/powers |
POST |
为主机上电。 |
/hosts/{id}/powers |
DELETE |
为主机下电。 |
/hosts/{id}/powers |
GET |
查询主机的电源情况。 |
在上例中,只描述了主机简单的电源情况。若需要描述更详细的电源情况,如主机上下电的历史信息、每次上下电的操作人等信息,可为上电和下电分别定义资源。
1.2.1 全量修改与增量修改
PUT和PATCH都可以表示修改,但分别表示全量修改与增量修改。其中的区别在于对未指定的数据的处理逻辑。
例如,主机包含名称和注释属性,当传递的消息中仅包括主机的名称信息而不包含注释信息时,若为全量修改,则需要将注释属性设置到初始状态(无任何内容);而若为增量修改,则应仅修改名称,而不修改注释。
另外需要注意的是,对于增量修改的API,参数中不包含指定属性和指定属性的值为null应表示不同的含义。不包含指定属性时时,其含义为不修改该属性;而若包含指定属性,且值为null,则其含义为将属性设置为null值(或初始值)。
但是通常情况下,对参数的解析(反序列化)通常由框架进行,如Spring MVC,此时在业务逻辑中已无法区分参数中是未设置某个属性,还是将属性设置为null值,更普遍的做法是只区分null和空值(如空字符串)。但是在协议层面上,这种做法本质上是有歧义的。这里的协议层面是指直接发送一个HTTP报文。因此为了避免这种歧义,应至少在文档层面上对这种处理方式进行解释和说明。
1.2.2 单个查询与批量查询
再以查询主机为例,一般来讲,“/host/1”与“/hosts?id=1”都是查询ID为1的主机,但其返回结果存在差异,前者返回的是一个主机实例的信息,而后者则返回的是一个列表,其中仅包含一个主机实例的信息。虽然其关键数据一致,但返回的数据结构不同。
通常情况,批量查询接口被称为“list”,即“列出资源信息”的含义。
2. 异常
2.1. HTTP状态码
在基于HTTP协议的RESTful API定义中,异常的定义应遵循HTTP的机制。在HTTP中,通过状态码来表示资源的状态。例如404表示资源不存在,410表示资源已经被删除等。常用的HTTP状态码有:
状态码 |
原因短语 |
含义 |
200 |
OK |
请求成功。常用于GET请求。响应中应包含资源信息。 |
201 |
Created |
请求成功,且有一个资源根据请求的内容被创建。常用语POST请求。 |
202 |
Accepted |
服务端已接受该请求,但后续的处理过程可能会失败。常见于请求的异步处理。通常情况下需要返回一个凭证,用以持续跟踪请求的处理情况。常见于POST、PUT、PATCH、DELETE请求。 |
204 |
No Content |
响应中没有响应内容,常用于DELETE请求。 |
205 |
Reset Content |
客户端应重新查询资源内容。常用于PUT、PATCH请求。 |
400 |
Bad Request |
请求格式不正确,应用于参数的合法性校验,通常也用于参数的有效性校验。 |
401 |
Unauthorized |
未授权,需要进行权限认证后再进行请求。 |
404 |
Not Found |
所需要操作的资源不存在,常用于PUT、PATCH、DELETE、GET请求。这里需要注意的是,一般来讲,即使是需要删除某个资源,此时资源不存在,也应使用该响应码。因为客户端可能需要区分资源是否是因自己的请求而被删除。 |
406 |
Not Acceptable |
无法提供合适的内容。通常情况下表示服务端已准备好资源,但是无法将这些内容转换为客户端所需的格式(请求的Accept头)。 |
409 |
Conflict |
被请求的资源因状态冲突而无法提供服务。例如资源处于不可修改状态,而客户端提交了PUT或PATCH请求。 |
410 |
Gone |
表示资源已被删除。与404的区别在于,404表示当前服务端没有该资源,该资源可能从未被创建。而410则更加明确该资源曾经存在过。一般情况下这种情况会被归为404一同表示。 |
500 |
Internal Server Error |
服务端发生未知错误。一般情况下,这个错误码表示服务进入了未处理的异常分支,通常仅用于表示代码BUG。 |
HTTP中还有很多其他的状态码,但一般不需要业务代码来感知。例如503所表示的服务不可用(Service Unavailable)通常用于服务应用的启动、升级等场景,但这个错误码通常情况下应被底层的应用程序框架或Web容器所处理。
2.2. 异常信息
一般来讲,在响应的HTTP状态码为4XX或5XX时,报文内容应承载异常信息,而非用户数据。而在状态码为2XX时,响应报文中应仅承载用户数据,而不体现异常信息的数据结构。
这与高级语言中的异常体系的思路是一致的。在高级语言中,如Java、C#等,当发生异常时,会抛出一个Exception,待调用方程序捕获处理,而不会在方法的返回值中表示是否发生了异常。
3. 示例
假定我们有一个应用的部署系统,需要提供应用的创建、修改、删除、查询、部署能力,那么这些API的交互过程应按以下方式完成。
3.1. 创建应用
当我们需要创建一个应用时,我们应当向服务端发送以下信息:
POST /apps HTTP/1.1 Content-Type: application/json;charset=UTF-8 Content-Length: 168 Accept: application/json;charset=UTF-8 Accept-Language: en-US Accept-Encoding: gzip, deflate
{ "name": "my-app", "archive": "http://cloud.com/archives/my-app.iso", "spec": { "cpus": 1, "memory": "100MB", "disk": "2GB" } } |
这个请求的首部中,我们指定了HTTP报文内容是一个JSON格式的文本,以UTF-8字符集进行编码,内容长度为168个字节。同时我们需要服务端给我们的响应也是一个JSON格式的文本,以UTF-8字符集编码,期望的语言为英语,可使用gzip或deflate算法进行压缩。
而请求内容中表示,我们所创建的应用名为“my-app”,归档件所在地址为“http://cloud.com/archives/my-app.iso”,应用所需的运行环境规格为1个CPU,100MB内存和2GB的磁盘空间。
服务端可能给我们返回以下内容的信息:
HTTP/1.1 201 Created Content-Type: application/json;charset=UTF-8 Content-Length: 392 Content-Encoding: gzip Date: Wed, 20 Jan 2021 23:41:56 GMT
{ "app": { "id": "01d59735-25e6-e227-23b8-926efbf34c82", "name": "my-app", "archive": "http://cloud.com/archives/my-app.iso", "spec": { "cpus": 1, "memory": "100MB", "disk": "2GB" }, "state": "available", "lastModifier": "Jishi Liang", "lastModificationTime": "2021/1/20 23:41:56" } } |
这个请求的首部中,指定了响应报文的内容是一个JSON格式的文本,以UTF-8字符集编码,长度为392个字节。使用gzip算法进行压缩,并在2021/1/20 23:41:56返回该响应。
响应的内容中为已经创建成功的应用的信息,除请求中所提交的信息外,还额外附加了应用的唯一标识、状态、创建人、创建时间等信息。
这里需要注意的是,响应的HTTP状态码为201(Created)。而且,一般来讲,响应中的数据会多于请求所提交的数据,并且这些数据通常是不可被修改的。
3.2. 部署应用
请求信息:
POST /apps/01d59735-25e6-e227-23b8-926efbf34c82/deployments HTTP/1.1 Content-Type: application/json;charset=UTF-8 Content-Length: 0 Accept: application/json;charset=UTF-8 Accept-Language: en-US Accept-Encoding: gzip, deflate
|
请求中表示,我们需要为应用“01d59735-25e6-e227-23b8-926efbf34c82“创建一个部署服务。这里的”部署“是一个资源,该资源会对应用进行部署。
HTTP/1.1 202 Accepted Content-Type: application/json;charset=UTF-8 Content-Length: 52 Content-Encoding: gzip Date: Wed, 20 Jan 2021 23:42:21 GMT
{ "id": "ea8bc7e1-07e4-8ca7-fa72-283a46efb122" } |
这个响应表示,服务端已接受了请求,并提供了部署的唯一标识,以供客户端后续跟进。通常情况下这个更多会返回一个异步任务的唯一标识。
3.3. 查询部署信息
请求信息:
GET /apps/01d59735-25e6-e227-23b8-926efbf34c82/deployments/ea8bc7e1-07e4-8ca7-fa72-283a46efb122 HTTP/1.1 Content-Type: application/json;charset=UTF-8 Content-Length: 0 Accept: application/json;charset=UTF-8 Accept-Language: en-US Accept-Encoding: gzip, deflate
|
这个请求中,我们通过首行的URI,告知服务端,我们需要查询应用ID为 “01d59735-25e6-e227-23b8-926efbf34c82”且部署ID为“ea8bc7e1-07e4-8ca7-fa72-283a46efb122”的部署信息。
响应信息:
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Content-Length: 260 Content-Encoding: gzip Date: Wed, 20 Jan 2021 23:43:38 GMT
{ "deployment": { "id": "ea8bc7e1-07e4-8ca7-fa72-283a46efb122", "app": { "id": "01d59735-25e6-e227-23b8-926efbf34c82", "host": "192.168.13.68", "port": 9019 }, "state": "deploying" } } |
这个响应向我们展示了当前的部署状态,应用部署在主机“192.168.13.68”上,且使用9019端口,当前依旧在部署中。
3.4. 修改应用
请求信息:
PATCH /apps/01d59735-25e6-e227-23b8-926efbf34c82 HTTP/1.1 Content-Type: application/json;charset=UTF-8 Content-Length: 45 Accept: application/json;charset=UTF-8 Accept-Language: en-US Accept-Encoding: gzip, deflate
{ "spec": { "disk": "4GB" } } |
这个请求中,我们通过PATCH请求对应用信息进行增量更新,将将所需运行环境的规格中的磁盘空间更新至4GB。所需修改的应用的唯一标识在首行中的URI中呈现。
响应信息:
HTTP/1.1 205 Reset Content Content-Type: application/json;charset=UTF-8 Content-Length: 0 Content-Encoding: gzip Date: Wed, 20 Jan 2021 23:53:32 GMT
|
这个响应告诉我们,信息已经被修改,我们需要重新查询来更新客户端所持有的数据。
3.5. 列出应用
请求信息:
GET /apps?offset=0&limit=100 HTTP/1.1 Content-Type: application/json;charset=UTF-8 Content-Length: 0 Accept: application/json;charset=UTF-8 Accept-Language: en-US Accept-Encoding: gzip, deflate
|
这个请求中,我们通过首行中URI的查询参数,指定我们需要查询所有应用中的第1~100条应用信息。
响应信息:
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Content-Length: 446 Content-Encoding: gzip Date: Wed, 20 Jan 2021 23:41:56 GMT
{ "apps": [{ "id": "01d59735-25e6-e227-23b8-926efbf34c82", "name": "my-app", "archive": "http://cloud.com/archives/my-app.iso", "spec": { "cpus": 1, "memory": "100MB", "disk": "4GB" }, "state": "available", "lastModifier": "Jishi Liang", "lastModificationTime": "2021/1/20 23:53:32" }], "offset": 0, "limit": 100, "total": 1 } |
响应中apps表示查询到的应用信息列表,offset和limit分别表示查询到的结果集在总结果集中的偏移量和限定长度,total表示总结果集中仅包含1个应用记录。
3.6. 删除应用
请求信息:
DELETE /apps/01d59735-25e6-e227-23b8-926efbf34c82 HTTP/1.1 Content-Type: application/json;charset=UTF-8 Content-Length: 0 Accept: application/json;charset=UTF-8 Accept-Language: en-US Accept-Encoding: gzip, deflate
|
这个请求中,我们通过URI中资源的唯一标识,来指定我们需要删除这个应用。
响应信息:
HTTP/1.1 204 No Content Content-Type: application/json;charset=UTF-8 Content-Length: 0 Content-Encoding: gzip Date: Wed, 20 Jan 2021 23:41:56 GMT
|
这个响应告诉我们,这个资源已经被删除,但是服务端没有需要返回给我们的数据。