多年前,我所在的一家大型电信公司开发了一个新型信息系统。我们必须通过旧系统或是友商与越来越多的 web 服务进行通讯。
更不用说,我们合理的拥有 SOAP Hell 的份额,玄奥的 WSDL ,不相容的 library ,奇怪的 bug ...所以只要可以,我们就提倡使用简单的远程过程调用协议:XMLRPC 或 JSONRPC 。
我们为这些协议提供的首批服务器与客户端非常基础,单调,脆弱。 但渐渐的,我们改善了它们; 通过几百行额外的代码,我们让所想变成现实:支持不同的方言(例如 Apache 特定的 XMLRPC 扩展),python 异常和分层错误代码之间的内置转换,功能和技术错误的单独处理,后续的自动重审,请求之前或之后的相关日志记录和统计信息,输入数据的彻底验证......
现在,我们只需要几行代码,就能和这样的API建立可靠的连接。
我们也只需要稍微修饰一下,做一些文档更新,就可以暴露一套新的功能给广泛的受众、服务器或者web浏览器。
对于应用间的通信(微服务风格),系统管理员自己就可以完成这些工作;对于软件层面,这几乎是透明的。
用了30分钟集成RPC API后,程序员在休息。
然后,REST出现了。
表述性状态转移(Representational State Transfer)。
一股复兴浪潮动摇了跨服务通信的根基。
RPC已死,未来是RESTful的:每个资源都有自己的URL,并通过HTTP协议进行操作。
然后,我们必须暴露或调用的API,成为了新的挑战;这简直愚蠢至极。
一个简短的例子就值得长篇大论。下面是一个小API,为了可读性删除了数据类型。
createAccount(username, contact_email, password) -> account_id addSubscription(account_id, subscription_type) -> subscription_id sendActivationReminderEmail(account_id) -> null cancelSubscription(subscription_id, reason, immediate=True) -> null getAccountDetails(account_id) -> {full data tree}
仅添加一个合适文档化的异常层次结构 (InvalidParameterError, MissingParameterError, WorkflowError…),使子类可识别重要的用例(例如AlreadyExistingUsernameError),这样你就可以了。
这些API易于理解、易于使用,并且是健壮的。他们是有精准的状态机支持,但有限的可用操作集使得用户远离无意义的交互(例如修改账户的创建日期)。
其它翻译版本 (1) 加载中将这个 API 暴露为一个简单的 RPC 服务,估计用时:几个小时。
现在,试试 RESTful。
没有太多的标准和规范,只有一个模糊的“RESTful哲学”,所以容易引起无休止的、形而上学的争论,还催生了许多不优雅的变通方案。
如何将上面明确的功能,映射为简单的 CRUD 操作?发送验证邮件,是更新一下"must_send_activation_reminder_email"属性,还是创建一个"activation_reminder_email resource"资源?如果在宽限期内,订阅仍然有效,或者可能恢复订阅,那用 DELETE 操作执行 cancelSubscription() 是否合理?如何在节点间拆分 getAccountDetails() 的数据树,来使它符合 REST 模型?
为每个资源分配什么 URL?是不难,但也需要实现。
如何使用非常有限的 HTTP 响应码,来表达错误场景的差别?
使用什么序列化、什么格式来描述输入输出?
HTTP 方法、URL、查询参数、负载、请求头、响应码,它们的分界线在哪?
花费了很多时间重复造轮子,甚至造的并不是好轮子。一个不完整的、易碎的轮子,需要通过大量文档来理解它,甚至不知不觉就违反了规范。
为什么 REST 带来了这么多工作(Work)?
这是一个悖论,也是一个双关(译者注:REST 在英文中有休息的意思)。
让我们深入探讨一下这个设计哲学所产生的人为问题。
REST 不是 CRUD,它的拥护者们不会让你混淆这两者。然而不久,他们会为 HTTP 已经提供了 CRUD 的语义而欣喜,例如创建(POST)、获取(GET)、更新(PUT/PATCH)和删除(DELETE)。
他们乐于承认这几个动词足以表达任何操作。嗯,当然是这样;就像用几个动词足以表达英语中的任何概念一样:“Today I updated my CarDriverSeat with my body, and created an EngineIgnition, but the FuelTank deleted itself(今天我用我的身体更新了我的汽车座椅,并且创建了一次发动机点火,但是油箱删除了它自己)”;这是不是有点尴尬。除非你是道本语的崇拜者。
追求极简是好事,但起码要做好。你知道为什么从来不在 Web 表单上使用 PUT、PATCH 和 DELETE 吗?因为它们百害而无一利。我们只需要用 GET 来读,用 POST 来写就好了。或者,当我们不需要 HTTP 层缓存时,只用 POST 就好了。其他方法好点的话也许会妨碍你,但最差的情况下会毁了你的一天。
想用 PUT 更新资源?可以,但是一些神圣的规范要求,数据输入必须与 GET 读取到的数据描述一致。那么,如何处理 GET 返回的大量只读参数呢(创建时间、最后更新时间、服务器生成的令牌……)?你准备忽略它们,而违反 PUT 使用原则吗?还是考虑它们,如果它们不符合服务端的值时,抛出"HTTP 409 Conflict"异常(强迫你再调用一次 GET……)?还是你给它们随机值,并祈祷服务端会忽略它们(沉浸在不会报错的喜悦中)?选个死法吧,REST 显然不知道什么是只读属性, 短期内也不会解决这件事。此外,用 GET 来返回密码信息(或信用卡号码)是危险的,之前都使用 POST 或 PUT;处理这些只写参数时,也只能祝君好运了。
我是不是忘了提 PUT 有可能会造成竞态条件,不同的客户端会覆盖其他客户端的变更,尽管它们只是想更新不同的字段。
又想用 PATCH 来更新资源?很好,但是,就像 99% 的人使用这个动词一样,只需要在请求负载中传递资源字段的子集,然后希望服务器能够正确地理解操作意图(和所有副作用);许多资源参数或是紧密联系,又或是相互排斥的(例如:在用户账单信息中,要么是信用卡号,要么是 PayPal 令牌),但是 RESTful 设计原则对这些重要信息避而不谈。不管怎么说,你会再次违反原则:PATCH 不应该只发送一堆需要被更新的参数。相反,应该提供给服务端一些指示信息来应用到资源上(译者注:不应该发一堆参数让服务端来猜,需要给服务一些提示,让服务器理解你的操作意图)。又到你了,拿上你的纸板和咖啡杯,你必须决定如何来表达这些指示信息。通常需要自定义规范,因为没有标准就是 REST 世界里的事实标准。(编辑注:REST 倡导者在这个问题上有所让步,提出了 Json Merge Patch,这是一个 Json Patch 的候选方案)
想用 DELETE 删除资源?好,但我希望你,不需要提供大量的上下文数据;就像用户对 PDF 扫描的终止请求。DELETE 不允许包含有效负载。这一点,REST 架构师经常忽略掉,因为大多数 Web 服务器不会对收到的请求强制执行这个规则。如果一个 DELETE 请求携带一个 2MB 的 Base64 字符串,怎么兼容规范呢?(编辑注:RFC 2616 指明了没有语义的负载应该被忽略,但现在已经废弃了)
REST 爱好者通常信奉“人们都做错了”,他们的 API“并不是真正的 RESTful”的。例如,许多开发者使用 PUT 直接在最终 URL 上创建资源(/myresourcebase/myresourceid), 然而,“正确的用法”(编辑注:根据很多人)应该是,在上一层URL(/myresourcebase)使用 POST 来创建资源,然后服务器通过 HTTP 的"Location"头来指明新资源的 URL(编辑注:不过,这不是 HTTP 重定向)。好消息是:这没关系。这些严格的准则就像是高位优先 vs 低位优先,它们让哲学家们费劲脑汁,但对现实没有什么影响,换言之,把事情做好就可以了。
顺便提一下……设计 URL 很有意思。你知道在构建 REST URL 时,有多少 urlencode() 的正确实现吗?如果没有,就等着接受 SSRF/CSRF 攻击吧。
当你忘记在 30 个 URL 中的其中一个对用户名进行 urlencode 时……
每一个编码者都可以使“名义上的用例”工作。错误处理就是这些功能里的一种,它将决定你的代码是否是健壮软件,还是一大推火柴棍。
HTTP提供了一系列开箱即用的错误码列表。好极了,让我们了解下。
使用“HTTP 404 Not Found”来通知那些不存在的资源,看起来很符合REST风格,难道不是嘛?太糟糕了:你的nginx被错误配置了一个小时,所以你的API消费者仅获得了404错误,并清除了数百个账户,他们认为这些账户已经被删除了...
我们的客户,在我们因为错误而删除其数G字节的重要镜像之后。
其它翻译版本 (1) 加载中当用户对一个第三方服务没有访问权限时,使用"HTTP 401 Unauthorized",这听起来是可以接受的,不是吗?但是,如果你在 Safari 浏览器的 Ajax 调用中获得此错误码,它会弹出一个密码输入框,这会让你的客户很吃惊[几年前,确实是这样的,你可能有不同意见]。
HTTP 比 RESTful 历史长得多,Web 生态系统充满了关于错误码含义约定俗成的东西。用 HTTP 状态码来表示应用错误,就像是用牛奶瓶装剧毒废物一样:总有一天会遇到麻烦。
一些标准的 HTTP 错误码是 WebDAV 专用的,另一些是微软专用的,剩下的有一些定义模糊,没有太大用处。最后,和大多数 REST 用户一样,你可能开始随意使用 HTTP 状态码,比如“HTTP 418 I’m a teapot”或未分配的数字,以用来表达应用程序中的特定异常。或者,厚着脸皮用“HTTP 400 Bad Request”来表示所有功能错误,然后用布尔、整型代码、slug 和翻译信息整合成笨重的错误格式,填充到负载中。或者,完全放弃正确的错误处理;只返回一个用自然语言描述的普通消息,并希望调用者能够分析问题,并采取行动。当与自治系统的这些 API 交互时,只能祝君好运了。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。 2KB翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务