写点什么

REST 反模式

2008 年 8 月 04 日

人们在试验 REST 时,通常会四处寻找样例——而他们往往不仅能找到一大堆自称“符合 REST”或标榜为“REST API”的样例,还会发现许多关于某个自称符合 REST 的特定服务名不副实的讨论。

为什么会这样?HTTP 虽不是什么新事物,但人们使用它的方式却五花八门。其中有些做法符合 Web 设计者的初衷,但许多并非如此。要为你的 HTTP 应用(无论是面向人类、还是计算机、或同时面向这两者使用的)应用 REST 原则,意味着你要恰好反过来:尽量“正确地”使用 Web,或者说按符合 REST 的方式使用 Web(倘若你不喜欢用对或错来评判的话)。对许多人来说,这的确是一种崭新的方式方法。

我经常在文章里作同样的声明:REST、Web 和 HTTP 是不同的事物;REST 可以用多种不同技术来实现,而 HTTP 只是一种恰好符合 REST 架构风格的具体架构。所以,其实我应该小心区分“REST”与“REST 式 HTTP”这两个概念的。但我没有这么做,在本文剩余部分,我们姑且认为它们是相同的事物。

跟任何新的方式方法一样,发掘一些共同的模式是有益的。在本系列的第一第二篇文章中,我已经讲述了一些基础——比如集合资源的概念、将计算结果转换为资源本身、以及用聚合(syndication)来模仿事件。后续文章将进一步讲述这些及其他模式。不过在本文中,我想主要说说反模式(anti-patterns)——即那些力求符合 REST 式 HTTP、但未能成功而造成问题的典型做法。

首先我们来看看我发掘了哪些反模式:

  1. 全部采用 GET
  2. 全部采用 POST
  3. 忽视缓存
  4. 忽视响应代码
  5. 误用 cookies
  6. 忘记超媒体
  7. 忽视 MIME 类型
  8. 破坏自描述性

下面我们来逐个详细说明。

全部采用 GET

在许多人看来,REST 仅仅意味着用 HTTP 暴露一些应用功能。HTTP GET 是最重要的基本操作(operation )(严格地讲,用“动词(verb)”或“方法(method)”这样的术语比较好)。GET 方法应当用于获取由 URI 标识的资源的一个表示(representation),而许多(即便谈不上所有)现有的 HTTP 库和服务器编程 API 不是将 URI 视为一种资源标识符(resource identifier),而是将之视为一种传递参数的便利手段。这导致了以下这种 URIs 的出现:

http://example.com/some-api?method=deleteCustomer&id=1234<br></br>实际上,你无法根据构成 URI 的字符获知关于给定系统的“REST 性(RESTfulness)”的任何信息,不过对于上面那个 URI,我们可以判断该 GET 操作不是“安全的(safe)”——也就是说,调用者很可能要为结果(删除一个客户)负责,尽管规范里说在这种情况下使用 GET 方法是错误的。

这种做法唯一有利的方面在于它编程起来容易,而且在浏览器中调试也简单——你只要把 URI 粘贴到浏览器地址栏里、然后调整一些“参数”就行了。这种反模式主要存在以下问题:

  1. URI 没有被用作资源标识符,而是被用于传递操作及其参数了。
  2. HTTP 方法(HTTP method)不一定跟语义相符。
  3. 这种链接一般不可加入书签。
  4. 有“爬虫”造成非预期副作用的风险。

注意:符合这一反模式的 APIs 没准最终碰巧符合REST 原则。这里有个例子:

http://example.com/some-api?method=findCustomer&id=1234<br></br>这个 URI 是标识操作及其参数呢,还是标识一个资源呢?两种情况都有可能:它可以是一个完全合法的、可加入书签的 URI;对它做 GET 操作也许是“安全的 ”;它也许会根据 Accept 报头返回不同的格式,并支持复杂的缓存机制。在很多情况下,这将是偶然的。API 经常在刚开始时采用这种方式来暴露一个“读 ”接口,但当开发者要增添“写”功能时就有问题了(因为你无法通过对上述 URI 做 PUT 操作来更新一个客户——开发者得构造另一个 URI)。

全部采用 POST

这一反模式跟前一个颇为相似,只不过这里用的是 POST 方法而已。POST 除了携带一个 URI,还携带一个实体主体(entity body)。一个典型的场景是:将单个 URI 作为 POST 请求的目标、通过发送不同的消息来表达不同的意图。实际上,SOAP 1.1 Web 服务就是这样做的,它把 HTTP 当作一种“传输协议”来用。服务器根据 SOAP 消息(可能还包括一些 WS-Addressing SOAP 报头)决定做什么。

可能有人认为“全部采用 POST”跟“全部采用 GET”存在的问题完全一样,只是它更难用一些,而且不能利用缓存(甚至连偶尔的机会都没有),且无法支持书签。事实上,它并不是违反了哪条 REST 原则,而是根本忽视了 REST 原则。

忽视缓存

即使你按各个动词的原本意图来使用它们,你仍可以轻易禁止缓存机制。最简单的做法就是在你的 HTTP 响应里增加这样一个报头:

Cache-control: no-cache<br></br>这样可以禁止缓存机制发挥作用。当然,这也许正是你想要做的,然而通常这只是你的 Web 框架规定的一个缺省设置。不过,对高效的缓存与再验证(caching and re-validation)的支持,是采用 REST 式 HTTP 的诸多关键优点之一。Sam Ruby 表示,在判断是否符合 REST 原则时的一个关键问题就是“你支持ETag 吗”?(ETag 是HTTP 1.1 里引入的一种机制,它允许客户端通过加密的校验和来验证一个被缓存的表示是否仍然有效)。要生成正确的报头,最简单的做法就是把这个任务交给一个“ 知道”怎样做的基础设施——例如通过在Web 服务器(比如Apache HTTPD)的目录里生成一个文件。

当然,这也要涉及到客户端一方:你在为一个REST 式服务实现程序客户端时,你应充分利用现有的缓存机制,以免每次都重新获取表示。例如,服务器也许已经发出信息:初次返回的表示在600 秒内都可被认为是“新的”(比方说因为后端系统每30 分钟才轮询一次)。这样的话,短时间内重复请求同一信息就完全没必要了。在客户端设置一个代理缓存(比如Squid)也许比自行构建相应逻辑更好。

HTTP 的缓存机制强大而复杂; Mark Nottingham 的《缓存指南(Cache Tutorial)》是一个很好的指南。

忽视响应代码

HTTP 提供了一组丰富的应用级状态代码,它们可用于应付不同场合,不过许多Web 开发者对此并不知晓。大部分人对200(“OK”)、404(“Not found”)和500(“Internal server error”)这些状态代码是比较熟悉的。但除此以外还有很多其他状态代码,正确使用这些状态代码意味着客户端与服务器可以在一个具备较丰富语义的层次上进行沟通。

例如,201(“Created”)响应代码表明已经创建了一个新的资源,其URI 在Location 响应报头里。409(“Conflict”)告诉客户端存在冲突,比如随PUT 请求发送的是基于老版本资源的数据。再如,412(“Precondition Failed”)表明服务器不能满足客户端的预期。

正确使用状态代码的另一方面涉及客户端:应该根据一种统一的总体方法对不同类别的状态代码(例如所有2xx 段代码、所有5xx 段代码)作不同处理——例如,即便客户端不具备处理特定代码的逻辑,但至少应把所有2xx 段代码视为成功信号。

许多声称符合REST 的应用仅仅返回200 或500,甚至只返回200(并在响应实体主体里给出错误文本——SOAP 就是这样的)。你要是愿意,可以称之为“通过状态代码200 传达错误”,但无论你觉得采用哪个术语好,假如你不利用HTTP 状态代码丰富的应用语义,那么你将错失提高重用性、增强互操作性和提升松耦合性的机会。

误用cookies

利用cookies 来传播某个服务端会话状态的键(key)是另一种REST 反模式。

Cookies 表明肯定哪个地方不符合 REST 了。是这样吗?不;不一定。REST 的关键思想之一是无状态性(statelessness)——不是说一个服务器不能保存任何数据:倘若是资源状态(resource state)或客户端状态(client state),那是可以的。服务器不能保存的是会话状态(session state),因为那会造成可伸缩性、可靠性及耦合方面的问题。Cookies 的最典型的用法是:保存一个跟“某个保存在服务端内存里的数据结构”相关联的键(key)。这意味着,浏览器随各次请求发出去的 cookie 是被用于构建会话状态的。

如果一个 cookie 被用于保存一些“服务器不依赖于会话状态即可验证”的信息(比如认证令牌),那么这样的 cookies 是完全符合 REST 原则的—— 不过有一点需要注意:如果有其他更为标准的方式来传递一则信息(比如放在 URI 里、放在某个标准报头里、或较少见地放在消息主体里),那就不应将之放在 cookie 里。例如,按 REST 式 HTTP 的观点来使用 HTTP 认证就比较好。

忘记超媒体

最不易接受的 REST 思想就是标准的方法集合。REST 理论并没有规定标准集合由哪些方法组成,它只是规定必须有一组适用于所有资源的方法集合。对于 HTTP 来说,这组集合是 GET、PUT、POST 和 DELETE(至少起初是这样),你需要一定适应时间才能掌握如何将所有应用语义投射到这四个动词上。但你一旦适应了,就可以开始运用这个 REST 的子集——一种基于 Web 的 CRUD(Create、Read、Update、 Delete)架构——了。暴露这种反模式的应用不是真正的“非 REST 式”应用(假如存在这种事物的话),它们只是未能利用一个 REST 核心概念——“ 超媒体即应用状态引擎(hypermedia as the engine of application state)”。

超媒体(hypermedia)是一个把事物链接起来的概念,正是它造就了 Web 这个网——一个互联的资源集合,应用通过跟随链接从一个状态进入另一个状态。这听上去也许有点深奥,不过其实遵从这一原则是有正当理由的。

“忘记超媒体”反模式的首要表现就是:表示(representation)里缺少链接。尽管通常客户端可以根据一定的规则来构造 URI,但是因为服务器没有发送任何链接,所以客户端将无法跟随链接。一种较好的做法是:即支持构造 URI,又支持跟随链接——这里的链接通常反映了下层数据模型中的关系。但最好的情况是:客户端应该只需知道一个 URI;其他 URI(各个 URI 及其构造模式,如:各种查询字符串)应该通过超媒体(作为资源表示里的链接)来传达。 Atom 发布协议(Atom Publishing Protocol)就是一个好例子,它有一个服务文档(service documents)的概念,服务文档为它所描述的域内的各个集合提供具名元素(named elements)。最后,应用可能经历的状态迁移应该是动态传播的,客户端应该可以不用掌握多少知识就可以跟随它们。HTML 就是一个好榜样,它包含足够的信息,以便浏览器可以向用户提供一个完全动态的接口。

我本想增加一个“人类可读的 URI”反模式的。但我没那么做,因为我跟其他人一样也喜欢可读的、好“篡改”的 URI。但是当人们采用 REST 时,他们经常浪费许多时间来讨论“正确的”URI 设计,而忘记了超媒体方面。所以,我建议你不要花太多时间来寻找正确的 URI 设计(毕竟,它们只是字符串而已),而是多花一些精力在表示里寻找提供链接的正确地方。

忽视 MIME 类型

HTTP 有个内容协商(content negotiation)的概念,它允许客户端根据需要获取资源的不同表示(representations)。例如,一个资源也许有不同格式的表示(如 XML、JSON 或 YAML 等)以便于用各种不同语言(如 Java、JavaScript 及 Ruby)实现的消费者所使用。再如,一个资源可能即有面向人类的 PDF 或 JPEG 版表示,又有“机器可读的”XML 版表示。还有,一个资源可能同时支持 v1.1 版和 v1.2 版的自定义表示格式。不管怎样,也许可以为“只有一个表示格式”找到理由,但这常常意味着丢掉某种机会。

显然,若一个服务能为更多未预见到的客户端所用(或重用)那更好。因此,依靠现有、预定义、广为人知的格式,要好过发明私有格式——这会导致本文讲述的最后一个反模式。

破坏自描述性

这种反模式是如此普遍,以至于几乎在每个、甚至那些由所谓的“REST 狂热者们”(包括我在内)创建的 REST 应用里都可以看到:违反自描述性约束(这一努力目标并不像人们最初想象的那样跟人工智能科幻小说有多大牵连)。理想情况下,一个消息(HTTP 请求或 HTTP 响应,包括报头与主体)应该包含足够信息,以便任何通用客户端、服务器或媒介(intermediary)能够处理它。例如,当你的浏览器获取某个受保护资源的 PDF 表示(representation)时,你可以看到由标准达成的协定是如何起作用的:有些 HTTP 认证交换发生,可能会发生一些缓存(caching)和 / 或再验证(revalidation),服务器发送的 content-type 报头( application/pdf )触发了你系统里注册的 PDF 阅读器,最后你得以在自己的屏幕上阅读该 PDF。所有用户都可以用他们自己的基础设施来执行同样的请求。若服务器开发者另外增加一种内容类型,那么服务器的客户端(或服务的消费者)只需确保他们安装了正确的阅读器即可。

你要是发明自己的报头、格式或协议,那就一定程度上破坏了自描述性约束。极端地讲,所有没有被某个标准化组织官方标准化的东西都违反此约束,因而可被认为符合本反模式。在实践中,你应努力做到尽可能遵循标准,并懂得“某些协定可能只在一个较小的领域(比方说,你的服务和客户端是专门针对它开发的)中适用” 的道理。

总结

自从“四人组(Gang of Four)”出版了书籍、掀起模式运动的开端以来,许多人误解了它,并试图在尽可能多的场合下应用模式——这已被其他人所取笑。模式应当仅在符合上下文时才被应用。同样地,可能有人会不遗余力地在所有场合下虔诚地努力避免所有反模式。许多时候,你有充分理由违反某一规则,或者按REST 的术语放松某一约束。这么做是没问题的——但了解实际情况、作出知情决策是有益的。

但愿本文能有助于你在开始首个REST 项目时避免落入这些常见的陷阱。

非常感谢Javier Botana 和Burkhard Neppert 对本文初稿的反馈。

Stefan Tilkov 是 InfoQ SOA 社区的首席编辑,以及位于德国 / 瑞士的 innoQ 公司的合伙人、首席顾问和主要的 REST 狂热主义者。

查看英文原文: REST Anti-Patterns


参与 InfoQ 中文站内容建设,请邮件至 editors@cn.infoq.com 。也欢迎大家到 InfoQ 中文站用户讨论组参与我们的线上讨论。

2008 年 8 月 04 日 07:253457
用户头像

发布了 63 篇内容, 共 22.2 次阅读, 收获喜欢 5 次。

关注

评论

发布
暂无评论
  • 响应状态码该怎么用?

    状态码是什么?它该怎么用?RFC标准将状态码分为了5大类,你都了解吗?

    2019 年 6 月 24 日

  • 争论:REST 需要描述语言吗?

    追踪上周在此讨论的关于REST vs. WS-*的争论,值得注意的是,以REST化服务契约为主题的争论在最近几天日嚣尘上。

  • 面向资源的架构:REST 的另一面

    这是面向资源的架构系列中的第一篇文章,在这篇文章里,Brian Sletten讨论了REST架构风格,SOA的历史,SOAP与WS-*,语义网,URL作为标识符,URI与URN,自由的形式,逻辑连接的延迟绑定系统,HATEOAS以及语义网对软件系统带来的影响。

  • 描述 RESTful 应用程序

    如果服务器不将它自己的名字空间控制在一个固定的资源层次下,客户端及更重要的客户端开发者将如何知道或发现资源的URI呢?在这篇新文章中,Subbu Allamaraju对如何描述RESTful API进行了讨论,文章重点集中于超媒体而不是诸如WADL或WSDL 2.0这类带外(out-of-band)描述格式的使用上。

  • REST 和分布式事务

    最近关于分布式事务及其在REST世界的位置的话题再次火热登场。很多人表明他们正在思考将事务与REST结合起来,或者正在进行中,然而还有一些人,包括Roy Fielding,认为这两个事物本就不该一起出现。

  • REST 会是 SOA 的未来吗?

    在本中文,Boris Lublinsky探讨了SOA和REST之间的架构差别并对使用REST机制实施SOA做了评估。

  • 如何获取(GET)一杯咖啡——星巴克 REST 案例分析

    在这篇文章里,Jim Webber、Savas Parastatidis和Ian Robinson展示了如何在REST式应用里运用超媒体来推动应用的工作流。他们通过Gregor Hohpe的经典案例“星巴克不采用两阶段提交”举例说明了怎样运用Web的思想进行集成。

  • 专访和样章试读:RESTful Web Services

    InfoQ发布了由Leonard Richardso和Sam Ruby联合撰写的“基于REST的Web服务”一书的样本章节。该书介绍了REST架构的原则,并解释如何使用Ruby on Rails、Restlel和Django构建基于REST 的应用。借此机会,InfoQ的Stefan Tilkov采访了该书作者,讨论关于写作该书的背景以及他们对REST和Web服务的看法。

  • 合并,替换,还是补丁:Astoria 如何应对变化的数据

    在使用REST时,当你执行一个PUT操作来更新已有的数据时会发生什么事?Astoria团队提出了这个问题,并给出了他们的答案。

  • RESTful 世界里的 Cool URI

    假想一下,如果要以最小的集成代价实现一个分布在全世界范围的信息空间,用它来共享机器可识别的数据,会怎么样?这是关于REST的吗?不是的。根据 SWEO的说法,这跟语义网有关。那些Cool URI有助于实现这种方式。所以,去看看RESTful SOA URI是不是也很“酷”可能是值得的。

  • 接口设计:系统间对话的语言,一定要统一

    今天,我就通过我遇到的实际案例,和你一起看看因为接口设计思路和调用方理解不一致所导致的问题,以及相关的实践经验。

    2020 年 5 月 5 日

  • 时势与英雄:HTTP 的前世今生

    你知道历史上哪些事件推动了HTTP的前进吗?它又促进了哪些技术的产生呢?

    2019 年 5 月 29 日

  • 如何实现真正的 REST 风格?

    Roy Fielding查看了SocialSite的REST API,发现它并不十分符合REST风格。

  • REST 是否会步 SOAP 的后尘?

    数周前,REST之争烽火重燃。起因是Pakal de Bonchamp撰文抨击了REST在多个方面存在问题,并称其将步SOAP的后尘。原文长篇大论,侃侃而谈,进而招致了大量评论。随后,WeWorK的Phil Sturgeon撰文逐项反击了Rakal的说法。以两人为主的论战仍在继续。

  • 文章:REST 反模式

    在本文中,Stefan Tilkov讲解了一些经常出现在自称“符合REST式设计”的应用中的反模式,并给出了避免这些反模式的对策。<a href="http://www.infoq.com/cn/articles/rest-anti-patterns" target="_blank">直接点击阅读完整文章</a>。

  • REST – 善,恶,丑

    关于REST的优势与缺点在业界的开发者中间引起了无尽的争论。Arnon Rotem-Gal-Oz的一篇新帖子提供了关于REST的“善”与“恶”两方面的思考。

  • CSRF 攻击:陌生链接不要随便点

    相信你经常能听到的一句话:“别点那个链接,小心有病毒!”点击一个链接怎么就能染上病毒了呢?

    2019 年 10 月 22 日

  • JAVA 削足适履适应 RESTful 设计

    在最近一篇Zapthink文章中,作者探讨了Java和JAX-RS2.0并不总适合于构建RESTful服务的原因。人们针对JAX-RS 2.0的改进谈了很多,但作者认为,最核心的问题之一是Java的对象模型。REST需削足适履以适应Java,反之亦然。

  • 你能写出正确的网址吗?

    只要你清楚了URI的格式,就能够轻易地“破解”地址栏里各式各样的长串字符了。

    2019 年 6 月 21 日

  • 安全性架构:为什么说用户密码泄漏是程序员的锅?

    系统安全是一个老生常谈又容易被忽视的问题,往往只有在系统被攻击了,数据泄漏了,才会关注软件安全问题。

    2020 年 1 月 31 日

发现更多内容

Week4 系统架构

贺志鹏

极客大学架构师训练营

架构师训练营第八周课程笔记及心得

Airs

架構師訓練營第 1 期 - 第 08 周總結

Panda

架構師訓練營第 1 期

Week_08 总结

golangboy

极客大学架构师训练营

架构第八周作业

Geek_Gu

极客大学架构师训练营

架构师训练营第 1 期 - 第 8 周 - 学习总结

wgl

第八周

等燕归

架构训练营 - 第8周课后作业 - 学习总结

Pudding

第四周作业

hunk

极客大学架构师训练营

架构师训练营第四周作业

邢永春

架构一期 第八周作业

haha

架构师一期

性能优化(二)

wing

极客大学架构师训练营

架构师训练营 - 第八周学习总结

chenlovehx

Week 8 作业01

Croesus

架构师训练营第四周总结

邢永春

架构师训练营 - 第 8 周课后作业(1 期)

Pudding

第八周作业及总结

solike

性能优化(文件、数据结构、算法、网络IO)

ABS

架构师训练营 2 期 Week04 总结

Calvin

第八周总结

fmouse

极客大学架构师训练营

漫画:一分钟快速了解VPN

网络技术平台

OpenVPN

第四周作业总结

hunk

极客大学架构师训练营

浏览器插件:那些你需会的操作

梁龙先森

Java chrome 前端 浏览器 前端工程

架构师训练营 2 期 Week04 作业

Calvin

第八 周 性能优化(二)总结

钟杰

极客大学架构师训练营

架构师 01 期,第八周课后作业

子文

架构师训练营第八周学习笔记

一马行千里

学习 极客大学架构师训练营

性能优化-文件硬盘I/O,数据结构算法,网络通讯

garlic

极客大学架构师训练营

动态规划 求最大连续子数组、Python range 函数指南、Postman 导出 curl命令、AWS知识图谱大赛架构设计、John 易筋 ARTS 打卡 Week 26

John(易筋)

动态规划 Postman ARTS 打卡计划 Range 知识图谱大赛

第八周作业

fmouse

极客大学架构师训练营

Week_08 作业

golangboy

极客大学架构师训练营

NLP领域的2020年大事记及2021展望

NLP领域的2020年大事记及2021展望

REST反模式-InfoQ