2020 Google开发者大会重磅开幕 了解详情

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:25 3420
用户头像

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

关注

评论

发布
暂无评论
  • 在 RESTful 应用程序中的超媒体

    “超媒体即应用状态引擎(hypermedia as the engine of application state)”是REST架构风格定义的约束之一。本文的作者是大名鼎鼎的Mark Baker,鼓吹使用REST风格代替主流Web服务方法的先驱者之一。在本文中,他讨论了超媒体约束在实践中意味着什么,以及为什么它对RESTful设计是不可或缺的。

  • REST 的缺点是什么?

    REST架构师邮件列表中最近的一篇帖子引起了Ganesh Prasad的兴趣,促使他总结了自己看到的REST(基于HTTP)在更动态的点对点环境中的若干问题,并提出了解决办法。他建议从Web Services处学习经验。他还提到自己一直致力于提出的Internet Draft规约。

  • 文章:REST 反模式

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

  • HTTP 服务

    2019 年 4 月 12 日

  • REST 和分布式事务

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

  • 应该如何理解请求方法?

    所谓的“请求方法”,它的实际含义就是客户端发出了一个“动作指令”,要求服务器端对URI定位的资源执行这个动作。

    2019 年 6 月 19 日

  • 在 RESTful 服务中实现部分更新

    近期Alex Scordellis发表了一篇文章,文章主题是如何针对客户端与RESTFul服务的交互进行建模和设计,实现部分资源的更新。如果能够对资源进行恰当的建模,这个问题似乎可以很容易解决。很多时候考虑到把资源作为实体来支持CRUD操作也是这个问题,包括把建模的资源作为“资源”和提供的服务。

  • 除了授权码许可类型,OAuth 2.0 还支持什么授权流程?

    今天我们一起学习OAuth 2.0中授权码许可类型外的其他3种许可类型:资源拥有者凭据许可、客户端凭据许可、隐式许可。

    2020 年 7 月 11 日

  • Restful Objects 简介

    Restful Objects是关于领域对象模型的超媒体API的公共规范。该规范的1.0.0 版本刚刚发布,并且目前已经出现了两个实现了该规范的开源框架——一个基于Java平台,另一个基于.NET平台。

  • 四通八达:HTTP 的重定向和跳转

    HTTP的重定向和跳转是怎么做到的?又有哪些应用场景?

    2019 年 7 月 8 日

  • 如何通过 gRPC 实现高效远程过程调用?

    这一讲以一个实战案例,基于前两讲提到的HTTP/2和ProtoBuf协议,看gRPC如何将结构化消息编码为网络报文。

    2020 年 6 月 15 日

  • 通过一组 RESTful API 暴露 CQRS 系统功能

    命令和查询责任分离Command Query Responsibility Segregation (CQRS)是一种将系统的读写操作分离为两种独立模型的架构模式。我们在此提出并展示一种为CQRS系统创建一套RESTful API的方式。这种方式结合了HTTP的语义、REST API基于资源的风格,并能够处理分布式计算的某些问题,例如最终一致性和并发性。

  • REST 会是 SOA 的未来吗?

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

  • Java 与.NET 的 SOA 互操作简易教程

    .NET和Java间的互操作可以通过以文档为中心的REST方式轻松实现。本文将REST和SOAP方式进行互操作做了对比,并比较了使用HTTP POST和GET做REST调用的优势。

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

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

    2019 年 10 月 22 日

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

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

  • WebSockets 与 REST 之争?

    随着WebSockets目前成为W3C的推荐候选,以及一个新JSR将会在JCP中启动,很多人不禁要问WebSockets是如何在REST协议下运作,那将是会怎样的情景?两者彼此是否兼容,或许正如有些人相信的那样,WebSockets是否会将公众的注意力从REST转移过去,从而引领一种新的Web交互风格?甚至已经有人认为,WebSockets将会“破坏web”。

  • 描述 RESTful 应用程序

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

  • 是时候将 WADL 加入到 JAX-RS 中了吗?

    在JavaOne2012上有一场关于Java EE未来的专题讨论,参会者很想知道WADL是否会成为JAX-RS标准的一部分。尽管讨论小组尚未同意,但是观众更倾向于支持WADL而不是反对它。这是不是一件好事儿呢?对于成功的REST来说,WADL是否会继续被视为非必需的呢?

  • JAX-RS,或者说 RESTeasy 不是 RESTful?

    JAX-RS是用Java编写RESTful应用程序的标准方式。然而,最近restfulie项目的领导Guilherme Silveira——这不是一个基于JAX-RS的项目——却对RESTeasy和JAX-RS是否是RESTful提出了质疑。

发现更多内容

IT培训机构那些不得不说的事儿

C语言技术网-码农有道

IT培训机构

新人怎么寻求解决问题的方法

波波

编程 职场 新人

从2009到2020,世界编程语言排行榜分析

C语言技术网-码农有道

编程语言

源码分析shared_ptr实现

this_is_for_u

c c++ C#

MyBatis 3 XML配置

杨家昌

mybatis mybatis-config.xml XML配置 配置

从草根到百万年薪C/C++程序员的二十年风雨之路

C语言技术网-码农有道

c++ 编程语言 C语言

产品周刊 | 第 13 期(20200503)

Herbert

产品 设计 产品经理 产品设计

LeetCode 565: Array Nesting

隔壁小王

算法

职场发展的思考

子不语

生涯规划 职业规划

如何消除写作过程中的痛苦,让写作变成一种享受

七镜花园-董一凡

写作

我们迫切需要块状时间

Neco.W

效率 时间分配 时间管理 工作效率 提升效率

Flutter 中SharedPreferences 的初始化?

三爻

flutter android

中年危机,我们如何面对?

石云升

言简意赅,聊聊RPC的123事

一叶而不知秋

RPC

游戏夜读 | 有哪几种游戏玩家?

game1night

Power BI商品管理应用:使用相关系数检核订单准确性

wujunmin

数据分析 Power BI Excel 零售 订单管理

部署Hexo博客到VPS

ini

LeetCode 153. Find Minimum in Rotated Sorted Array

隔壁小王

算法

1分钟理解M2M和IoT概念

老任物联网杂谈

物联网 M2M IoT

Power BI抓取豆瓣热门电影数据

wujunmin

数据分析 爬虫 Power BI Excel 豆瓣

DataGrip常用快捷键

fliter

企业招聘的需求决定了C/C++程序员的学习方向

C语言技术网-码农有道

C/C++

new[]和delete[]为何要配对使用?

this_is_for_u

c c++ C#

原创 | 类应该是匀称和均匀的

编程道与术

原创 | 使用JUnit、AssertJ和Mockito编写单元测试和实践TDD (一)什么是单元测试

编程道与术

MyBatis 3 解析mybatis-config.xml配置

杨家昌

深度解析 mybatis 初始化 XMLConfigBuilder Configuration

零基础、非计算机相关专业的如何转型程序员

C语言技术网-码农有道

程序员 转型

断章取义,不一样的C/C++语言的学习策略

C语言技术网-码农有道

C/C++

终端Terminal:程序员是如何查询天气预报的?

lmymirror

GitHub 工具 命令行 terminal 终端工具

基于大疆无人机SDK二次开发

sydMobile

android 无人机

Linux初学-01

蜗牛前进

2020中国技术力量年度榜单盛典

2020中国技术力量年度榜单盛典

REST反模式-InfoQ