人们在试验 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、但未能成功而造成问题的典型做法。
首先我们来看看我发掘了哪些反模式:
- 全部采用 GET
- 全部采用 POST
- 忽视缓存
- 忽视响应代码
- 误用 cookies
- 忘记超媒体
- 忽视 MIME 类型
- 破坏自描述性
下面我们来逐个详细说明。
全部采用 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 粘贴到浏览器地址栏里、然后调整一些“参数”就行了。这种反模式主要存在以下问题:
- URI 没有被用作资源标识符,而是被用于传递操作及其参数了。
- HTTP 方法(HTTP method)不一定跟语义相符。
- 这种链接一般不可加入书签。
- 有“爬虫”造成非预期副作用的风险。
注意:符合这一反模式的 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 中文站用户讨论组参与我们的线上讨论。
评论