restful API 转自知乎

https://zhuanlan.zhihu.com/p/27458970

 

最近(很久前)在设计 API 接口的时候发现了一些很难取舍的地方,就看了下业界领先的企业都是怎么设计类似 API 的,发现了很多之前设计的缺陷和一些新的思路。主要参考了 AWS 和 Google Cloud 的 API 设计,两家的设计可以说是把两个不同的风格发挥到了极致,做同样的事情 API 的各个方面都可以设计的完全不一样。Google 有一个自己的 API Design 规范可以在网上找到,不过真实的 GCE API 规范略有一些出入。而 AWS 的 API 设计是没有什么文档的,只能通过现有的 API 进行逆向工程来反推设计理念。这篇文章会比较琐碎,更像是一个读书笔记,细节的东西会比较多。

Google Cloud API

由于 Google 的文档比较详细了,就先照着 Google 的文档来讲,而 Google 的 API 是按照 REST 风格来的,而 REST 只是一种纸面的模式,每家的实现可能都不一样,我们就来看下 Google 眼里的 REST 是什么样的。然后介绍一下 REST API 的一些局限和缺点以及 Google 的解决方式。

最权威的 REST 当然还是得去看论文了,不过对于普通开发者来讲 HTTP 的 REST 就是一个 URL 指向一个资源,然后这个 URL 上通过不同的 HTTP 方法来实现对这个资源的不同操作。简单来说就是指向资源的 URL + HTTP 方法就构成了常见的 REST API。这里面每一个部分都有很多门道。

URL

URL 可以分成好几个组成部分

1. 域名,不同的域名可以区分这个网站提供的不同 API 服务。比如一个图书,一个电影,域名就应该是  和 


2. 版本号,对应不同的历史版本,可以是 v1,v2,也可以是 alpha,beta 这样

3. 资源集合,书店里可能会有多种资源的集合,比如图书,报纸,影碟,这就需要用资源集合来区分 /books, /newspapers, /cds 等等

4. 资源名称,这时候才真正到具体的资源,一个图书可能就是 

5. 子资源,有时候一层的资源模型不能满足现实的需求,比如图书馆的检索是按照书架来的,需要先知道在哪个书架才能找到书,就需要在中间插一层资源。最后的 url 可能就变成了 

6. 重复 3 - 4

资源命名

1. 资源名必须是合法的 C 语言变量名,这个规范主要是为了代码和 SDK 的一致性,不然有个减号这样的字符很多语言的 SDK 里这个资源对象你就不得不换个名字或者写法了,很容易造成不一致,一些自动生成 SDK 的工具也会失败

2. 资源集合必须是复数

3. 不要用缩写避免不必要的歧义

4. 不要用过于泛化的资源类型,像 type, object,element,resource 这样的命名一来不是很清楚,二来很容易和编程语言的关键字撞名字,人为增加编程难度。方法
常用的 HTTP 方法有 GET, POST, PUT, DELETE 稍微常见的还有 HEAD,PATCH, 不太常见的还有 COPY, LINK, LOCK, VIEW 等等。由于 HTTP 是纯文本的协议,并没有规定只能用哪几个方法,只要服务端做处理就可以自己再自定义其他方法的,比如 Google 自己就实现了一个 BATCH 来做批量处理。

每个 HTTP 方法和以对应不同的语义,而且这些语义像 GET 是获取, POST 创建 PUT 更新 DELETE 删除基本上大家都是形成共识的,API 最后的统一性会很好。REST 的初衷也是让资源尽可能的多,每个资源的方法尽可能的只有标准的几个方法,这样所有的 API 看起来长得都很像,学习和实现的成本都会很低。下面说一下具体的每个方法需要注意的一些事情

List

1. 需要用 GET 方法 url 指向资源集合,并返回资源的列表

2. List 返回的内容最好有分页信息,而不是简单的一个资源列表,便于前端的展示并减少每次拿所有数据的性能消耗

3. 资源列表需要是有序的,如果两次请求同样资源顺序完全不一样前端再不处理就会有奇奇怪怪的现象

4. List 需要提供简单的按照某个字段排序,filter 方法,更复杂的过滤查询需要单独的方法

5. 可以返回一些 metadata,比如资源数量,资源集合的信息等

Create

1. POST 方法 URL 指向资源集合,创建成功需要返回这个资源,而不是只有一个 201 的状态码

2. 需要允许 client 自己指定 resource_id,而不是全部由系统自动生成。这样第三方系统可以根据情况进行重试和重复检查。不然一个 create 请求中间路径上有重试就会生成多个 id 不同的资源,后续处理会很麻烦。

Update


1. PUT 或者 PATCH 方法 URL 指向具体资源,更新成功需要返回资源实体

2. PUT 一般用于整个资源实体的更新,而 PATCH 只更新某个字段。对于一个复杂的有大量字段的资源,最好两种 API 都提供,而不是只提供一个 PUT。只提供一个 PUT 即使只更新一个字段也需要传递完整的资源实体,很多情况下是没必要的

用户自定义方法


由于 HTTP 的标准方法是有限的,而很多语义是标准方法不能表示的,比如需要把书从一个书架移动到另一个书架就很难用标准方法表示,这就需要用户自定义方法。定义 HTTP 方法理论上没问题,但很多第三方库并不支持这种方法,这种 API 给别人看也会比较奇怪,所以通常会把方法加在 url 里,通常是加在资源名称后面。比如移动一本书就可以是 /books/book_1:move

1. 通常的做法是用『 /』 来做 URL 的分隔,但是在 Google 的规范里用的是用『 :』 因为 / 分隔会有歧义,不知道后面的到底是一个子资源还是一个用户自定义方法。听起来很有道理的样子,但是在 GCE 的 API 里并没这么做也是用的『 /』

2. 尽管 URL 里有了自定义方法,HTTP 方法还是尽量用符合语义的,比如更新类的操作都用 PUT

3. 现实中很多操作都是标准 HTTP 方法表示起来很苦难的,比如重启机器,发送邮件,账号登录这些,可以想一下这些操作如何用 HTTP 标准方法来设计 API

标准字段


不同的资源有许多共同的属性字段,由于每个资源对应的 API 可能是由不同人在不同时间完成的,所以需要有个约定好的公共字段的命名,不然到最后会出现混乱的情况。比如资源需要有个 id 表示,就会出现不同资源有的叫 uuid,有的叫 id,有的叫 resource_id;创建时间又有 created_at,created_datetime,created_time,分页又有 page_token, next_page, page_num 诸如此类不一致的情况。同样一些字段的类型也需要统一,比如时间使用时间戳还是标准时间,带不带时区。

除了正常的返回就是错误返回也要有统一的格式。错误信息需要包含 error_code, error_message 和 error_detail。其中 error_message 是用来返回给最终用户的,而 error_detail 则用于内部人源进行错误排查。

REST 的缺陷


尽管 REST API 的设计原则在现实中使用的很广,但是这种设计也是有很多局限性的。

1. 层级设计很容易过多,比如在公有 IAAS 中需要获得一 private_ip 的信息,这个 ip 可能是属于某块网卡的,网卡属于某个机器,机器属于某个 subnet,subnet 属于某个 vpc,vpc 属于某个 region。设计出来的 api 就变成了 /regions/region_id/vpcs/vpc_id/subnets/subnet_id/instances/instance_id/nics/nics_id/private_ips/private_ip_id 这种 API 直接上就会觉得不合理。而且仔细考虑机器是不是应该属于 subnet, private_ip 是不是应该直接是 subnet 的子资源,会有很多需要考虑的地方。

2. 标准 HTTP 方法大多是操纵整个资源,而这个粒度在一些情况下太粗了。比如同样是更新一个主机的操作,更新主机的一个 tag 和更新主机的 name 都是 PUT 一个资源实例 url,而更改安全组将主机状态设置成停止也是同样的 PUT 方法和 url 就会有些不合理。尽管都是更新操作,但是更新的字段不同,功能不同,重要性也不同,用同样的大 PUT 就会不合适。而且这些字段可能会对应着不同的权限操作,如果任何更新都是同样的方法,之后细粒度的权限控制也会变得十分痛苦。

3. 涉及多资源的操作会很难设计。REST 的思想是对资源进行操作,但现实中一个操作可能会对应多个资源,这时候对于多个不同资源关系的操作 REST 设计起来就比较头疼了。比如给一个主机加一个 eip,那么这个操作肯定要用到自定义方法了,接下来的问题就是这个方法到底是属于主机的,还是属于 eip 的还是两个资源都需要加一个同样的方法?而且 API 的设计也会影响实现的思路,在设计时 API 是把主机和 eip 当成独立的资源,那么实现的时候很可能就忽略了他们俩之间是有关联关系的,很可能到最后就会出问题。

4. 批量操作变得困难。由于 REST API 的更新和删除 url 路径都是指向具体一个资源的,批量的更新删除就需要额外进行设计。比如批量关闭几个主机,API 的样子就会和其他的完全不一样。如果不设计批量操作的 API 只是组合使用单个资源的 API,当需要操作的实例多的时候很容易出现性能问题,而且如果操作时间过长,那么中间各种冲突,不一致的几率就会大幅上升。

Google 的应对方案


1. 层级多的问题,翻一下 google cloud 的 api 可以发现大部分的 api url 只有三层,前两层还都是 project 和 zone,一个实例的 url 就是 /v1/projects/project/zones/zone/instances/instance 换句话说就是真正到了云里面的资源就没有再分层了,所有的资源层级都是扁平的在同等地位,所有的资源都是关联关系而不是层级关系。这样就避免了层级过深和考虑如何设计层级的问题,也保证了将来每个资源操作的灵活性。

2. 大量的自定义方法。尽管 REST 本来的目的是大量的资源少量的标准操作,很显然这个显示环境中是有困难的。Google 在 instance 这个资源的自定义方法就有 20 多种,最后的情况就是大量的资源加上几个标准方法再加上大量的自定义方法。通过自定义方法,每个方法对应一个操作,权限控制也可以对应到每一个方法上,细粒度的权限控制也就可以实现。

3. HTTP BATCH 方法。至于批量操作的问题 Google 是通过自定义 HTTP 方法实现的,之前说过了不要用 自定义 HTTP 方法因为大家不支持,可是 Google 这种能对 Web 标准起到影响的企业就任性了。BATCH 方法其实就是在 HTTP 的 body 里再封装多个标准的 HTTP 方法求情。这样批量操作还是一个个的操作,不过可以一次性打包发过去不用一个个发送了。API 也不用单独再设计一套批量的,还能组合不同类型的操作,不过一般企业就不要学这种作风了。

尽管 Google 很详细的做了 API 设计的文档,但是如果你去看国内 IaaS 厂商的 API 文档会发现没有一家是这么做的,甚至连一点 REST 的模样都没有。Google 的 API 设计看上去最符合开发者的常规思维,在这个领域反而显得和一个另类一样,为啥子呢?如果你看过 AWS 的 API 就会发现国内这些厂商大概都是从 AWS 那边借(chao)鉴(xi)过来的。至于 AWS 是如何应对 REST 世界中的类似问题以及他到底长什么样子,可能下篇会写。

参考资源

1. Google API Design Guide

posted @ 2020-02-21 11:58  Szz  阅读(404)  评论(0编辑  收藏  举报