理解RESTful API 架构设计规范与实践
本文介绍了 REST 的由来,对 REST 的风格架构设计指导原则做了详细的说明。同时举例了过往开发中若干细节的考虑和实现方案。
RESTful 架构是目前流行的一种互联网应用架构。如果把网站,移动应用从服务器到前端,从整体上看作是一个软件,它就是一个层次清楚,功能强,扩展方便,适宜通信的架构规范。
论文发表于2000年,作者在基于 REST 的约束上设计了 HTTP 协议。设计 REST 的目的,就是为了指导现代 Web 架构的设计与开发。他是 HTTP 协议(1.0 版和 1.1 版)的主要设计者、Apache 服务器软件的作者之一、Apache 基金会的第一任主席。
REST 最初被 称作“HTTP 对象模型”,但是那个名称常常引起误解,使人们误以为它是一个 HTTP 服务 器的实现模型。这个名称“表述性状态转移”是有意唤起人们对于一个良好设计的 Web 应 用如何运转的印象:一个由网页组成的网络(一个虚拟状态机),用户通过选择链接(状态转移)在应用中前进,导致下一个页面(代表应用的下一个状态)被转移给用户,并且呈现给他们,以便他们来使用。
第四章描述了设计 REST 的动机:“为 Web 应该如何运转创建一种架构模型, 使之成为 Web 协议标准的指导框架”。第五章从一个没有约束的空架构开始,不断的添加约束,从而使此架构进化为 Web 所需要的架构。
REST 约束包括:客户-服务器,无状态,缓存,统一接口,分层系统,按需代码。如果一个架构符合 REST 原则,就称它为 RESTful 架构。
当我们用这个原则来设计服务器端的接口,为前端或者外部提供数据时,就称它为 RESTful API 。目前观察到,业内在实践中有下面这些指导原则:
用统一资源标识符来标识资源应用状态引擎的超媒体(HEOAS)使用标准的 HTTP 方法安全性和幂等性无状态性
REST 是一种面向资源的架构,它能更好的适应分布式下的系统架构设计。对于开发者来说,越来越简单,越来越灵活。
任何能够被命名的信息都能够作为一个资源,它是对信息的核心抽象:一份文档、一张图片、一个与时间相关的服务(例如:“我现在城市的天气”)、一个包含其他资源的集合、一个非虚拟的对象(例如:用户)等等。它是到一组实体的概念上的映射,而不是实体本身。
更精确地说,资源 R 是一个随时间变化的成员函数 MR(t),该函数将时间 t 映射到等价的一个实体或值的集合,集合中的值可能是资源的表述和,或资源的标识符。
在设计具体 API 的时候,资源是业务系统里,抽象出来的一个业务对象。例如:用户(User),订单(Order),令牌(Token)等等。它允许随时间变化,输出不同值。
一个资源具有一个或者多个标识。这里说的标识就是统一资源标识符(Uniform Resource Identifier,URI)。统一资源定位符(Uniform Resource Locator,URL)则是 URI 的一种具体实现,是 URI 的子集。在通常的 API 设计中,直接使用 URL 来标示应用系统中的资源。
在标准的 REST 规范观点中,通常会要求将资源 shop 加上复数形式 s 表示多个资源。但现实中,很多时候忘记给获取多个资源的接口加上复数形式,在不影响理解的前提下,这种做法也不是不可以接受。
移动应用的首页,通常有多种资源:横幅广告、编辑精选的内容、针对当前用户推荐的商品,相应的对应着三个资源:广告(banners),内容(contents),商品(products)。
通常后端的开发会让前端调用这三个资源的接口,获得相应的数据。但是前端开发会要求,后端能不能针对首页单独给一个接口?
为适应这种情况,我们会创建一个首页的资源:index,这个资源包括需要展示的其他子资源。如:
这样的接口设计就沿袭了 RPC 的设计风格,表示 users 服务提供的 search 方法,不符合 REST 规范,也是规范里建议的,URI中不要有动词。因为资源表示一种实体,所以应该是名词,URI不应该有动词,动词应该放在HTTP协议中。
这个名词也非常的拗口。它实际的意思是,在资源的表述中,如果有其他资源的,则会提供相应的链接 URL,使得用户不查文档,也知道如何获得相关的资源。
Github 的 API 就是这种设计,访问会得到一个所有可用的 API 的信息列表,类似这样:
在设计的理想状态中,使用 HATEOAS 的 REST 服务中,客户端可以通过服务器提供的资源的表达来智能地发现可以执行的操作。当服务器发生了变化时,客户端并不需要做出修改,因为资源的 URI 和其他信息都是动态发现的。
这样的设计,保证了客户端和服务器的实现之间是松耦合的。客户端需要根据服务器提供的返回信息来了解所暴露的资源和对应的操作。当服务器端发生了变化时,如修改了资源的 URI,客户端不需要进行相应的修改。
在实践中,我没发现哪家国内的公司公布的 API 的接口遵守了这条原则,我们自己开发时,也不实现这条原则。为什么实际中大部分开发不遵守这条原则呢?
HTTP 能实现 RESTful,是因为浏览器只是将表述以及对资源的操作选项展示了出来,至于具体该如何操作,是由使用浏览器的人来决定的。也就是说,虽然服务端告诉了客户端操作的可选项,但是客户端没办法知道该选择什么。网页浏览是有人参与的,但是 RESTful API 没有人参与,导致 RESTful API 的客户端难以做出决定,该做什么。
鉴于现实这个尴尬情况,Richardson 提出了「REST成熟度模型」。该模型把 REST 服务按照成熟度划分成 4 个层次:
Level 0:Web 服务只是使用 HTTP 作为传输方式,实际上只是远程方法调用(RPC)的一种具体形式。SOAP 和 XML-RPC 都属于此类。Level 1:Web 服务引入了资源的概念。每个资源有对应的标识符和表述。Level 2:Web 服务使用不同的 HTTP 方法来进行不同的操作,并且使用 HTTP 状态码来表示不同的结果。如:HTTP GET 方法来获取资源,HTTP DELETE 方法来删除资源。Level 3:Web 服务使用 HATEOAS。在资源的表述中包含了链接信息。客户端可以根据链接来发现可以执行的动作。从成熟度模型中可以看到,使用 HATEOAS 的 REST 服务是成熟度最高的,也是推荐的做法。但实际的落地实现时,多数都是次一级的做法。
RESTful API 使用标准的 HTTP 协议实现前后端的接口调用。对涉及到的资源,常用的操作就是增、删、改、查,类似对数据库记录的CRUD(Create,Read,Update,Delete)。使用的 HTTP 方法规则如下:
在修改的时,PUT 和 PATCH 区别在于 PUT 是全量修改,user 资源有多少信息,需要全部提供,而 PATCH 可以只修改手机或者邮箱,昵称,密码等信息。
例如,腾讯云的对象存储的API:HEAD Bucket 请求可以确认该存储桶是否存在,是否有权限访问。
使用场景:客户端先使用 OPTIONS 询问服务端,应该采用怎样的 HTTP 方法以及自定义的请求报头,然后根据其约束发送真正的请求。
例如,腾讯云的对象存储 OPTIONS Object 接口实现 Object 跨域访问配置的预请求。即在发送跨域请求之前会发送一个 OPTIONS 请求并带上特定的来源域,HTTP 方法和 Header 信息等给 COS,以决定是否可以发送真正的跨域请求。
完全按 REST 指导原则采用标准 HTTP 方法很美好。但是实践中,各大平台推出了内置小程序,前端在调用小程序的 request 请求时,发现只支持 GET、POST 方法。
这种情况下,导致后端的 API 接口不得不做调整,所有方法在设计之初就只用 GET/POST 方法。
或者在已成型的系统中使用扩展属性:X-HTTP-Method-Override,一个非标准的HTTP协议头,来绕过这个问题。前端统一使用POST,在请求头带上这个非标属性,服务端根据Header:X-HTTP-Method-Override,转换成真正的 METHOD。
OPTONS、HEAD、GET 既是幂等也是安全的,不修改资源,多次调用对资源的影响是相同的。POST、PATCH 既不幂等也不安全,修改了资源,同时多次调用时,对资源影响是不同的,PATCH 的影响不同在于,每次的局部更新可能会导致资源不一样。PUT 是对资源的全量更新,多次更新总是对资源影响是一致的,所以它是幂等,但不安全。DELETE 用于删除资源,多次调用的情况下,都是删除了资源,所以它是幂等,但不安全。
幂等性原本是数学中的含义,表达式的N次变换与1次变换的结果相同。为什么要在接口设计时,考虑幂等性?
订单创建接口,前端调用超时了,但服务端已经完成了订单的创建,然后前端显示失败,用户又点了一次。用户完成了支付,服务端完成了扣钱操作,但前端超时了,用户不知道,又去支付了一次。用户发起一笔转账业务,服务端已经完成了扣款,接口响应超时,调用方重试了一次。
以上类似的场景,需要在设计接口时考虑幂等性。我们可以借鉴微信支付的接口方案来实现这类场景需要的幂等性。在支付之前,需要调用一个接口生产预支付交易单,获得一个交易单号,随后再针对这个交易单号完成支付。服务端确保一个交易单号只会被支付一次,这样就保证了支付过程的幂等性。
在对幂等性的理解上,有时候我们会有疑惑:服务端一般会有日志、缓存或者数据表上的计数器、最后更新时间等。这样上面说的 GET、PUT、DELETE 符合幂等性的方法就会导致这些数据内容的变化,是不是就不是幂等性的方法呢?
我认为在这个幂等判断问题上,还是要回到什么是资源的定义问题上来。服务端的日志、缓存、计数标志、更新时间等,不属于抽象出来的核心概念,也就是对资源没有本质上的影响,这些方法仍然是幂等的。
客户端与服务端的交互必须是无状态的,并在每一次请求中包含处理该请求所需的一切信息。服务端不需要在请求间保留应用状态,只有在接受到实际请求的时候,服务端才会关注应用状态。
这种无状态通信原则,使得服务端和中介能够理解独立的请求和响应。在多次请求中,同一客户端也不再需要依赖于同一服务器,方便实现高可扩展和高可用性的服务端。
REST 只维护资源的状态,并不维护客户端状态,而且 HTTP 本身是无状态的。那如何在用户登录后,和服务器之间传递用户信息呢?
以前这类场景下的做法是使用 cookie 和 session,两者的区别在于,是存在客户端还是服务端。这种设计,就违反了无状态通信的设计原则。实践中,API 设计成在用户登录之后,服务端会返回客户端一个用户令牌(Token),并带有失效时间,客户端在后续的接口请求都会带上这个令牌。
API 上线运行后,就会有版本升级的情况。在 API 设计中,如何体现不同的版本?有两种通常的做法:
网上也有另一种说法,就是不同版本的资源,仍然还是资源的不同表现形式,所以 URL 应该是同一个,版本号需要放在 HTTP 信息头的 Accept 字段中。
REST 设计原则并未提到有些需要权限的业务场景下应该怎么做。在实践中,推荐使用 OAuth 2.0 标准来实现 API 的鉴权需要。具体如何实现,篇幅可能需要比较长,不在此赘述。
例如需要对列表信息进行约束。应用在初始时只显示开始的若干条记录,等待用户翻页或者下拉操作后,需要服务端从第几页开始,返回一页多少条记录。类似下面这样:
403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
实践过程中,不推荐 API 设计采用上面的错误返回码。这样做的结果会把业务系统里面本身的错误状态与 HTTP 协议本身的错误混淆起来,有悖于架构设计中分层的原则。例如:
前端收到一个 404 错误时,不清楚是服务端没有这个接口,还是请求的业务系统中的对应资源不存在。
出现 500 错误时,是不是服务端的服务器问题,还是创建资源时,不满足业务系统的一些限制条件。
所以,在最终的实际运行的系统中,只保留了一个200状态码,表示服务端收到了请求,并做了处理,业务系统本身的错误返回信息在结果中体现。
上一节说了服务端返回时 HTTP 状态码时统一使用 200,表示业务系统正常运行。如果有系统的错误提示信息需要向调用者返回,则在返回结果中统一定义。如下面是一个常见的返回对象:
第二种方案对用户更友好,常见用于调用外部接口时,屏蔽掉让用户不明白的错误提示信息,显示遇到错误时,应该如何处理的友好提示。
实际系统开发过程中,接口文档也是非常重要的一环。如果单独专门编辑接口文档,时间一长,就会造成代码和文档的不一致情况。开发团队还需要专门费事费力的在代码版本升级后,同时更新接口文档。所以最好的办法,就是写代码的时候,同时更新文档。
如果使用 Spring boot 开发的朋友,可以使用 swagger 自动生成在线的接口文档,并且支持自动生成调用参数,在线测试开发好的接口。
其他语言的文档生成工具,可以试试 apidoc。也是通过读代码中的约定格式的注释,自动生成 API 文档,也支持在线测试调用。
以上,大致将 RESTful API 架构设计原则做了一个框架性的介绍,附带介绍了具体项目实践中的几种落地的做法。
技术发展到现在,在具体的代码实现上,Java 的 Spring boot 框架已经很成熟,能满足所有 RESTful API 设计原则。
- 标签:本站
- 编辑:唐志钢
- 相关文章
-
理解RESTful API 架构设计规范与实践
本文介绍了 REST 的由来,对 REST 的风格架构设计指导原则做了详细的说明
-
30款前端特效源码分享
我们经常在抖音上看到一些前端很酷的特效,诸如:程序员女朋友相册之类,看着是不是很酷炫呢?其实只要有源码,自己进行图片的更换,你也…
- 春节补课!十大品牌笔记本型号命名规则详解
- 不会写代码依然可以做出好看漂亮的网页掌握几种方法就可以了
- 前端开发规范(四、JS篇)
- 前端入门教程网页导航栏制作教程(技术:HTML+CSS)
- 新闻:济源哪有WEB前端培训机构【2022更新中】