RESTful API设计实践

在当前程序设计语言层出不穷,技术架构五花八门的现在,系统间交互已经渗透到程序设计的方方面面。作为当前最成熟的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

 

这个响应告诉我们,这个资源已经被删除,但是服务端没有需要返回给我们的数据。

原文地址:https://www.cnblogs.com/talefox/p/14305941.html