RESTful 被认为是有害的

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡 / 赠书活动

目前, 星球 内第2个项目《仿小红书(微服务架构)》正在更新中。第1个项目:全栈前后端分离博客项目已经完结,演示地址:http://116.62.199.48/。采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 276 小节,累计 43w+ 字,讲解图:1917 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 1500+ 小伙伴加入,欢迎点击围观

我不喜欢 RESTful 原则和 API。近年来,它被视为进程间通信的通用协议,尤其是在分布式系统中。然而,我看到 REST 的许多不足之处,并且有一些替代方案适用于某些用例。显然没有 一刀切 ,我只是想强调 REST 架构在很多方面都有缺陷。

需要额外压缩的臃肿的、人类可读的格式

REST 事实上 的标准格式是 JSON。至少它比具有 XML 和信封的 SOAP 要好得多。大量网络带宽被浪费(可能是移动设备或大型数据中心的问题)或 CPU 用于压缩和解压缩。一个可爱的报价:


[the] internet is running in debug mode 源代码

这是一个严重的问题,询问高频交易员 解析基于文本的 FIX 有多难。有很多既易于解析又使用很少内存的完善的二进制协议,例如 Protocol buffers Avro Thrift 。此外,它们内置了向后兼容性。从技术上讲,您可以 将 Google Protocol Buffers 与基于 Spring MVC 的 REST 服务一起使用 ——但不知何故很少有人这样做并坚持使用 JSON。

当然,JSON 有一个(字面意思是 一个 )巨大的优势——它是人类可读的。好吧,至少只要它 印得漂亮 ——很少有这种情况。但是我们真的要付出那个代价而不是默认使用二进制格式并在需要人工干预时使用翻译器或开关吗?

既不是模式也不是契约

我属于静态打字阵营。我喜欢我的编译器在我运行单元测试之前就发现了错误——而且我一开始就不需要写那么多错误。此外,特别是在函数式编程中,如果代码可以编译,它很可能会起作用,因为类型强制执行的合同非常严格。在处理 XML 时,我很高兴 XSD 得到验证,这样我就可以确定我阅读的内容符合合同。我可以减少断言的数量并使代码更短。我生成的 XML 也保证在语法上是正确的。最后但同样重要的是,在使用关系数据库时,严格的模式可以防止我通过插入不正确的值来破坏数据。与 XML 类似,我也可以相信我读到的内容:外键是正确的,NOT NULL 列确实不为空,等等。这些显然是机器强制契约的优势。不知何故,在设计由分布式系统中的机器生成和使用的 API 时,所有这些都不再重要(并且意外地在许多无模式的 NoSQL 数据库中,如 MongoDB)。

可以理解的对 SOAP 的仇恨将我们推向了另一个极端——根本没有合同。没有广泛的标准来记录合同并为 REST API 自动执行它们。这些天我发现的大多数 API 只是示例请求和响应的集合,充其量是 Swagger 。我永远不知道哪些字段是可选的,格式和约束是什么。有人可能会争辩说这是设计使然,我们没有将自己耦合到一个具体的 API,从而允许它发展。但我有一种感觉,大多数 API 使用者仍然是耦合的,一旦推出未记录和未宣布的更改,它们就会中断。

发布 URI

谈到文档,纯粹主义者声称 API 应该公开的唯一信息是根 URI,例如

www.example.com/api

.其他一切,包括允许的方法、资源、内容类型和文档,都应该通过 HATEOAS 发现。如果 URI 没有正式记录但应该被发现,这意味着我们不应该在代码中依赖硬编码 URI,而是每次使用此类 API 时都遍历资源树。这是惯用的,但也很慢且容易出错。更不用说 Swagger,REST 文档的 事实 标准,官方声称这种方法 不是按设计 ——并且坚持使用固定的 URI。

不支持批处理、分页、排序/搜索……

RESTful Web 服务本身并不支持 API 的许多企业级功能,例如批处理请求、分页、排序、搜索等。有一些相互竞争的建议,比如查询参数、请求标头等。我记得前段时间我们就灵活搜索 API 进行了长达一个小时的讨论。应该有:


  • 单个类似 SQL 的参数(使用适当的转义!),例如 query=age>10 and (name=John or company=Foo)
  • 每个条件的多个参数,例如 age=10&name=John&company=Foo (但如何实施 OR 运算符?)
  • 最奇怪的是: /searches 上的有状态 POST,标准采用类似 JSON 的结构建模,将 URL 返回到搜索结果( /searches/25 ),稍后可以查询

REST 在这里真的很受限制。

CRUD 的定义

RESTful Web 服务是面向 CRUD 的,而不是面向业务或事务的。无数次我们不得不仔细地将业务术语映射到简单的 创建 / 更新 / 删除 操作中。世界并不是那么简单,并不是所有的事情都可以用 创建或更新 句子来简单描述。即使可以,RESTful 端点通常也非常笨拙和人为。你还记得 POST 到

/searches

?此外,并非所有数据都可以映射到 URI 树结构中,并且我们通常允许非规范化,例如,同一资源在多个 URI 下可用,以便于访问。

想象一个发布领域事件、任意状态变化的系统,例如

LoanApproved

,

EmailSent

等等。当然,每个事件都有自己的一组不同的属性。您如何设计使用此类事件的 RESTful API?不可避免地你会得到

POST /domainEvents

接受任意 JSON,可能带有一些标签

"type": "LoanApproved"

.因此,您有一个采用任意 JSON“BLOB”的端点,并且很可能有一个巨大的类似开关的语句来正确解释各种类型的事件。代替

"type"

"method"

你刚刚重新发明了 JSON RPC 。您将如何以惯用的方式做到这一点?将发布者端的每个域事件转换为适当的 API 调用?听起来不是很健壮。

HTTP 动词描述性不够

从业务术语映射到 POST/PUT/PATCH/DELETE/HEAD 是乏味和幼稚的。就像 URI 一样,世界要丰富得多,并不是所有东西都适合这些桶。这些动词旨在与万维网中的文档和表单进行交互。使用 REST 感觉就像将我们的业务领域降级回数据库浏览器。没有逻辑,没有领域驱动设计,没有丰富的流程。我们只是简单地操纵物体,来回移动它们。当然,REST 不必也不应该直接映射到数据库实体。但即使它们映射到您的核心业务领域,您仍然必须通过有限的 CRUD 界面查看您的领域。

将 HTTP 状态代码与业务回复混合

通过 REST 惯用地发出业务错误信号应该使用 4xx 类状态代码。然而,这些错误并非旨在指示业务案例,因此我经常发现自己试图将 4xx 代码映射到业务结果。验证码测试失败?让我们把它变成 400 Bad request 。表单验证错误? 417 期望失败 ?由于重复而违反约束? 409 冲突 ?但是乐观锁异常呢?客户余额不足怎么办?如果......这些错误代码是为文档检索而设计的,而不是后端背后的丰富业务。您最终会将所有内容置于相同的状态代码下或构建复杂的翻译文档。我们发明了异常、错误、

Either<T>

- 只是为了突然将自己限制在一组固定的数字错误代码上。

404 尤其成问题,因为它非常普遍。使用 RESTful API 时,您永远无法判断 404 是业务情况还是只是 URL 中的拼写错误。听起来像是分布式调试地狱的秘诀。哦,我有没有提到没有编码错误的标准方法?

时间耦合

RESTful API 在微服务爱好者中非常流行。但让我们回到基础。 RESTful API 基于 HTTP,HTTP 是构建在 TCP/IP 之上的请求-响应协议。 TCP/IP 在面向数据包的 IP 协议之上构建连接抽象。 IP 几乎无法将消息从一台机器传送到另一台机器。通过构建所有这些抽象级别,我们忘记了 Web 实际上是异步的。我们相信,通过使用无契约的松散 JSON,我们不再将系统耦合在一起。但是耦合还有另一个维度:时间依赖性。如果一个系统需要将某个事件通知另一个系统,那么它们是否真的需要同时存在。在某些情况下,通常使用 GET 实现,请求-响应是有意义的。但在大多数情况下,我们真正想要的是即发即弃、至少一次的语义。事实证明,消息驱动的分布式架构更加健壮和容错。两个系统不再需要同时看到对方和生活。此外,如果一个系统产生太多请求,它就不会再破坏另一个系统。只需在两个系统之间放置一个持久队列。这种方法有很多优点:


  • 生产者和消费者可以随时重启而不会丢失数据或降低服务质量
  • 可扩展性更容易实现,不需要复杂的负载均衡
  • 我们可以一次将消息发送到多个系统
  • 使用 窃听模式 调试 可能 更容易

RESTful 也没有单向请求的概念。没有身体的 POST 尽可能接近。这是有问题的,因为许多业务案例自然符合即发即弃的语义。当我发布域事件时,我不关心响应,但 REST 服务与 HTTP 协议耦合得太紧了。

不过,只有典型的请求-响应交互(如通过 ID 检索用户)并不适合队列。具有相关 ID 的时间队列很笨拙,并且会引入大量延迟。

没有标准

没有标准,只有好的,有时是相互矛盾的做法。我们甚至无法就资源应该是单数还是复数达成一致,更不用说分页参数、错误处理、HATEOAS……根据 Richardson 成熟度模型 ,你会发现人们在争论你的服务如何 RESTful。缺乏标准意味着每个人都可以将他们的服务命名为 RESTful。它可能是 Roy Fielding 论文 中的一个活生生的例子,它可能是一个

<form>

POST 处理程序——只要它们使用 HTTP,它们就是 REST。这也意味着您现在不知道如何正确地与任何 API 交互。您应该使用哪些标头、如何解码响应、如何协商内容类型(标头?URL 中的扩展名?)以及支持哪些类型。

向后兼容性

RESTful 服务在处理向后兼容性方面几乎没有缺陷:

  • 只添加字段,从不删除或更改。我亲眼目睹了这样一种情况,有人修复了一个恰好直接编码为 JSON 的对象中的无辜拼写错误。这使另一个系统崩溃
  • 具有版本控制的内容类型 - 痛苦并且需要维护多个版本
  • 如果资源被重命名,则 HTTP 重定向 - 仅在客户端足够 RESTful 时才有效,完全避免硬编码 URL

上面的每一种技术都有其自身的问题,RESTful 服务根本就不是为不断发展而设计的。

备择方案

在通过 HTTP 或 REST 嬉皮士风格的集成跳转到 JSON 之前,我希望您问自己几个问题:

  • 性能是一个问题吗?然后选择不需要昂贵压缩的更紧凑的格式
  • 你真的需要阻塞请求响应吗?如果不是,请考虑消息队列或存储,如 Kafka
  • 您是在进行持续部署还是自动缩放?然后选择暂时不耦合客户端和服务器且无连接的技术,见上文
  • 您的交互是否复杂,或者您可能正在将现有的 API/接口从模块提取到分布式服务?然后选择更接近经典 RPC 的技术
  • 你需要 exactly-once语义 吗?如果是这样,对不起,没有任何帮助

PS:强制性附录: 《Considered Harmful》Essays Considered Harmful