We FALL ASleep At Night, We Do REST Right

时间:2023-11-28 23:19:32

Github 同步发表链接

前言

笔者在上一篇文章中提过,任何一种非“强制性”约束同时也没有“标杆”工具支持的开发风格或协议,最后都会在不同的程序员手中得到不同的诠释,微服务是如此,DDD 是如此,笔者把它称为技术思想上的“康威定律”。不出意外的,REST 同样难逃此劫。光是在学习和收集资料的过程中,笔者就已经见过不下十多篇此类理解,甚至于在 url 中使用短划线或下划线连接单词也是众口难调。

尽管这只是小事。

微软也发布过关于如何设计 REST API 的开发指南,但是不幸的是,REST 的创始人 Roy Fielding 认为微软的 REST API 规范与 REST 没有多大关系。

“即使是我最糟糕的 REST 描述也比微软的 API 指南提供的总结或参考要好很多。”

那什么才是正确的 REST 描述呢,或者说,REST 是什么。本文的创作动机便是希冀于解决这样一个问题。

本文假设读者已经具备基本的 REST 和 Web 知识,哪怕你们现在认为 HTTP API 就是 REST API 也可。

REST 起源

REST 英文全称为 Representational State Transfer,又名“表述性状态移交”,是由 Roy Fielding 在《架构风格与基于网络的软件架构设计》一文中提出的一种架构风格(Architectural Style)。而在这篇 REST 圣经问世之前,R.F 博士就已经参与了 HTTP 1.0 协议规范的开发工作(1996年),并且负责了 HTTP 1.1 协议规范的制定(1997年)。

一种架构风格由一组准确命名的,相互协作的架构约束组成。当我们在谈论 REST 本质的时候,我们谈论的其实是架构约束。

REST 用以指导基于网络的分布式超媒体系统的设计和实现,Web(即万维网)就是一种典型的分布式超媒体系统。可以确定的是,在制定 HTTP 协议的过程中,R.F 博士就已经以 REST 架构风格作为指导原则来完成相关工作。论文中提到了以下内容:

“在过去的6年中,我们使用 REST 架构风格来指导现代 Web 架构的设计和开发。这个工作是与我所创作的 HTTP 和 URI 两个互联网规范共同完成的,这两个规范定义了在 Web 上进行交互的所有组件所使用的通用接口。”

“自从1994年起,REST 架构风格就被用来指导现代 Web 架构的设计与开发。”

“开发 REST 的动机是为 Web 的运转方式创建一种架构模型,使之成为 Web 协议标准的指导框架。”

“REST 的第一版开发于1994年10月至1995年8月之间,起初,在我编写 HTTP/1.0 规范和最初的 HTTP/1.1 建议时,将它用来作为表达各种 Web 概念的一种方法……”

Web 架构规范主要包括 HTTP, URI 和 HTML 等。

所以我们也不难理解为什么 REST 与 Web 和 HTTP 能够结合得如此紧密。尽管直到2000年,这只“鸡”才在下完鸡蛋后,出现在了世人面前。

We FALL ASleep At Night, We Do REST Right

REST 约束

无论是否愿意承认,REST 一开始就是为 Web 而服务的,可以这么说的是,REST 是现代 Web 的架构风格,Web 也是 REST 最典型和最成功的案例。包括在 R.F 博士的论文中,他也是在解决现代 Web 需求(无法控制的可伸缩性和独立部署)的过程中而逐步推导出 REST。前文已经提到一种架构风格是由一组准确命名的,相互协作的架构约束组成。而所谓架构约束,便是这个推导过程中最重要的产物。甚至高于 REST 本身。

早先的 Web 与 REST 所描述的模型有着大量出入,然而正是在对应的 HTTP 和 URI 规范出炉后,才有了所谓“现代 Web”的说法。笔者更愿意把“现代 Web”的定义期限定为1996年后。

客户端 - 服务端

设计与实现上的关注点分离。

无状态

在客户端没有发起请求时,服务器并不知道它的存在。同样的,服务器无须维护当前请求之外的客户端状态,从而改善服务器的可伸缩性。Session 和 Cookie 都是“需要”被抛弃的。

如果有些应用状态重要到服务器需要去关心,那它应该成为一个资源。

缓存

对于客户端而言,使用缓存则是维护状态和提升性能的更好做法。

统一接口

使 REST 架构风格区别于其他基于网络的架构风格的核心特征是,它强调组件之间具有一个统一的接口。实现与他们所提供的服务是解耦的,这促进了独立的可进化性。同时这也引申出了其他的约束:资源识别;通过表述来操作资源;自描述信息;超媒体作为应用状态引擎(即 HATEOAS)。下文会专门说明。

分层系统

“分层系统”约束在“客户 - 服务端”约束的基础上增加了代理组件和网关组件。尽管笔者认为代理和网关都不是重点,“分层系统”约束更注重的是“在客户端和服务端之间添加一个组件应该是一个透明操作”,组件只能“看到”与其交互的相邻层(是不是想到了[迪米特法则][5]),使用层级来封装服务,同时能够支持负载均衡和诸如安全性检查的功能。

按需代码

这是六大约束中唯一的可选约束。REST 允许客户端通过下载并执行脚本或其他形式的代码,对客户端的功能进行扩展,从而提高客户端的灵活性和性能。通俗点说,HTML 中的 `` 标签就是一种按需代码,尽管它可能会导致一些例如跨站脚本攻击这样的问题。

统一接口约束

R.F 博士在论文中针对六大约束中的“统一接口”做了额外的约束分解和说明,但遗憾的是并没有以列表的方式展示出来。但在接下来的内容中你可能就会发现,这几项可能是目前大部分开发者践行 REST 原则时所遵循的全部标准。

资源识别

REST 对于信息的核心抽象是资源,任何能够被命名的信息都可以称为是资源,只要你的想象力允许。资源一词通常和“可寻址性”绑定,一个或多个 URI 标识一个资源。如果资源的 URI 发生了变化,服务器应该使用超媒体引导客户端访问新的 URI 或提示对应信息。

通过表述来操作资源

当客户端对一个资源发起一个请求时,服务器会以一种有效的方式提供一个采集了资源信息的文档作为回应。这就是表述——一种以机器可读的方式对资源当前状态的说明。客户端和服务器之间也可以继续传递表述,从而对资源执行某种操作。客户端从来不会直接看到资源,能看到的都是资源的表述。可以这么说的是,服务器发送的表述用于描述资源当前的状态,客户端发送的表述用于描述客户端希望资源拥有的状态,这就是表述性状态转移/移交。

一个表述由一个“字节序列”和描述这些字节的“表述元数据”构成,且不与服务器端代码绑定,这意味着当服务器端的资源实现和业务操作代码发生变化时,可以选择不更改资源的呈现方式。

值得注意的是,一般人通常会将表述认为成资源的“值”,这虽然可以理解,但是当你请求一个天气服务时,千万不要认为表述一定便是温度等确定的值信息,因为它仍然可能是某次响应中的错误提示。一个表述的具体含义取决于消息中的控制数据。

“控制数据定义了在组件之间移交的消息的用途,例如被请求的动作或相应的含义。它也可用于提供请求的参数,或覆盖某些连接元素的默认行为。例如,可以使用(包含在请求或响应消息中的)控制数据来修改缓存的行为。”

“表述的数据格式称为媒体类型(media type)。发送者能够将一个表述包含在一个消息中,发送给接收者。接收者收到消息之后,根据消息中的控制数据和媒体类型的性质,来对该消息进行处理。”

表述在现代 Web 中的实例包括 HTML,Json,XML,图片等。

自描述的消息

一个 (HTTP)消息体包含了所有足以让接收者理解它的必要信息,在现代 Web 中,自描述的消息由一些标准的HTTP方法、可定制的HTTP头信息、可定制的HTTP响应代码组成。扩展开来,它通常有以下三方面的含义。

  1. 请求之间的交互是无状态的。 对应于 REST 约束中的“无状态”约束,服务器可以独立处理每个请求,而无须对该客户端先前所有请求的处理进行记忆。
  2. 使用标准的方法和媒体类型来表达语义和交换信息。想想 HTTP Methods 和 HTTP Headers,客户端通常靠这些信息理解请求的含义和解析消息体。
  3. 响应可以明确地表明其可缓存性。

超媒体作为应用状态引擎

该约束便是大名鼎鼎的“HATEOAS”(Hypertext/Hypermedia As The Engine Of Application State),但实际上 R.F 博士在论文中并没有对它做过详细的介绍。在目前的共识中(讽刺的是在大多数时候它并没有被应用到设计所谓 REST APIs 中去),HATEOAS 意味着客户端应该使用超文本来作为你在接收到当前的表述后,再进行下一步寻址的方式。更进一步的,客户端需要通过解析超文本理解服务器提供了哪些资源,而不是在客户端事先定义或约定俗成。

“客户端依赖的是超文本的状态迁移语义,而不应该对于是否存在某个URI或URI的某种特殊构造方式作出假设。一切都有可能变化,只有超媒体的状态迁移语义能够长期保持稳定。” —— 《理解本真的REST架构风格》

最终结果便是客户端可以自动化地适应服务器端的变化,服务器也允许在不破坏所有客户端的情况下更改它底层的实现。同样的,我们可以列出几点说明。

  1. 所有的应用状态维持在客户端一侧。改变应用状态是客户端的职责。
  2. 客户端仅能够通过发送请求和处理响应来改变应用状态。
  3. 客户端可以通过已经收到的表述中的超文本知道接下来可以操作的请求动作(如 HTML 中的超链接)。
  4. 超文本是应用状态变化背后的动力。

看起来,上述四点内容说的多是集中式 Web 应用的情况,在如今多用 Web APIs 进行前后端分离开发的 Web 应用中,HATEOAS 又该做如何理解呢?如今有这么一项技术可以让超文本继续充当驱动应用状态更新流动的引擎,那就是 Web Links,RFC 5988 定义了 HTTP 的这项扩展。

Github REST API v3 中,我们可以在很多 apis (如列表翻页)的响应体中看到 Link Header,对应引导的 Uri 同样有相关标准,即 Uri Templates(RFC 6570)

Link: <https://api.github.com/user/repos?page=3&per_page=100>; rel="next",
<https://api.github.com/user/repos?page=50&per_page=100>; rel="last"

至此,想必你也大致清晰了 HATEOAS 的含义。

如果可以的话,你可以将“应用状态”理解为客户端对资源操作后的展示结果。比如“主页”,“博客”,“关于我”,“成功提交”等操作界面。它和“资源状态”有抽象概念上的区分。


你可以放弃对 Hypertext 和 Hypermedia 之间区别的思考,笔者也认为它们在你理解 REST 时并不应该区分。

Richardson 成熟度模型

Richardson Maturity Model 是一个可以尝试的甜品,特别是当你在设计自己的 REST APIs 时。和 REST 相似,你也可以把该模型称为指导原则。

We FALL ASleep At Night, We Do REST Right

笔者无意去解释这几个层级,因为这些层级和“统一接口”的扩展约束都是间接的映射关系。而且正如上文一直在强调的,REST 不依赖于任何单一的通信/传输/移交协议,所以模型中的 HTTP 指示就有些耐人寻味了。

“它是标准吗?”

“当然不是,它只是目前设计 REST APIs 时的一种潜规则。”

总结

对于理解概念性文章的总结总是特别艰难,看起来内容挺丰富,同时结合了 R.F 博士论文及其译者李琨教授相关文章,其实摊开了目录整篇文章也就只涉及到了起源—>约束->模型这样几个方面,实际上 REST 也确实只是一组约束而已。最后,仅用笔者认为的 R.F 博士论文中至关重要的两段话作为结束。

“因此,REST的模型应用是一个引擎,它通过检查和选择当前的表述集合中的状态迁移选项,从一个状态迁移到下一个状态。毫不奇怪,这与一个超媒体浏览器的用户接口完全匹配。然而,REST风格并不假设所有应用都是浏览器。事实上,通用的连接器接口对服务器隐藏了应用的细节,因此各种形式的用户代理都是等价的,无论是为一个索引服务执行信息获取任务的自动化机器人,还是查找匹配特定查询标准的数据的私人代理,或者是忙于巡视破损的引用或被修改的内容的维护爬虫。”

“这个名称“表述性状态转移”是有意唤起人们对于一个良好设计的Web应用如何运转的印象:一个由网页组成的网络(一个虚拟状态机),用户通过选择链接(状态迁移)在应用中前进,导致下一个页面(代表应用的下一个状态)被转移给用户,并且呈现给他们,以便他们来使用。”

有效的参考文档

  1. 理解本真的REST架构风格
  2. 理解本质的 REST
  3. REST APIs must be hypertext-driven
  4. HATEOAS作为领域特定协议描述的引擎
  5. Richardson Maturity Model