WEB开发中,使用JSON-RPC好,还是RESTful API好?

还有其他优秀的推荐方案吗?
关注者
3,129
被浏览
769,461

81 个回答

简单来说:不管哪个“好”还是不“好”,RESTful API在很多实际项目中并不实用。因此真的做了项目,你可能会发现只能用HTTP+JSON来定义接口,无法严格遵守REST风格。

为什么说不实际呢?因为这个风格太理想化了,比方说:

  • REST要求要将接口以资源的形式呈现。但实际上,很多时候都不太可能将一些业务逻辑看作资源。即使强制这么干了,也会非常非常别扭。登录就是登录,而不是“创建一个session”;播放音乐就是播放,而不是“创建一个播放状态“。
我们之所以要定义接口,本身的动机是做一个抽象,把复杂性隐藏起来,而绝对不是把内部的实现细节给暴露出去。REST却反其道而行之,要求实现应该是“资源”并且这个实现细节要暴露在接口的形式上。

但一个好的接口设计就应该是简单、直观的,能够完全隐藏内部细节的,不管底层是不是资源,资源的组合还是别的什么架构。此外,让业务逻辑与接口表现一致,对系统的长期维护和演进都有极大的好处。
  • REST只提供了增删改查的基本语义,其他的语义基本上不管。比如批量添加,批量删除,修改一个资源的一部分字段。区分“物理删除”和“标记删除”等等。复杂的查询更加不显示,对于像筛选这类的场景,REST明显就是个渣。这里要表扬一下GraphQL(但GraphQL有其他的问题,在此不展开)
  • REST建议用HTTP的status code做错误码,以便于“统一”,实际上这非常难统一。各种业务的含义五花八门,抽象层次高低不齐,根本就无法满足需要。比如一个404到底是代表这个接口找不到,还是代表一个资源找不到。400表达请求有问题,但是我想提示用户“你登录手机号输入的格式不对“,还是“你登录手机号已经被占用了“。既然201表示“created”,为啥deleted和updated没有对应的status code,只能用200或者204(no content)?错误处理是web系统里最麻烦的,最需要细心细致的地方。REST风格在这里只能添乱。
  • web请求参数可能散布在url path、querystring、body、header。服务器端处理对此完全没有什么章法。客户端和服务器端的研发之间还是要做约定。
  • 在url path上的变量会对很多其他的工作带来不良影响。
比如监控,本来url可以作为一个接口的key统计次数/延迟,结果url里出了个变量,所以自动收集nginx的access log,自动做监控项目增加就没法弄了。
再比如,想对接口做流量控制的计数,本来url可以做key,因为有变量,就得多费点事才行。
  • 现实中接口要处理的真正的问题,REST基本上也没怎么管。比如认证、授权、流控、数据缓存(http的etag还起了点作用)、超时控制、数据压缩……。
  • REST有很多好的工具可以便利的生成对应的代码和文档,也容易形成规范。但问题是REST在实际的项目中并没有解决很多问题,也在很多时候不合用,因此产生的代码和文档也就没什么用,必须经过二次加工才能真的用起来。因此可以基于REST+你的业务场景定义一个你自己的规范。

REST的本意是基于一个架构的假设(资源化),定义了一组风格,并基于这个风格形成约定、工具和支持。思路不错。但是因为他的架构假设就是有问题的,因此后续一系列东西都建立在了一个不稳固的基础之上。同时,REST并没有解决太多的实际问题。

是,的确,有些时候,用REST完成CRUD已经能完成任务了。此时,用REST没有什么不好的。但是,现实当中,真正的业务领域一般都会比资源的CRUD复杂的多。这时REST“基本上没解决太多实际问题”的缺点就会体现出来。我所见到的大多数情况,是会形成一种REST-like形式的接口,像REST却又不限于REST。

为了REST,我看到了太多的人在争执到底是POST还是PUT,到底用querystring还是body,到底用200还是201,到底一个单词应该用单数还是复数,到底一个请求参数应该放在url path的中间一段还是最后一段…… 真正要做的事情本身反而没人关心。而一旦把争论给一个“REST专家”看,他的回答八成是“其实你还是不懂REST”...

我觉得人生不能这么糟蹋,你觉得呢?

---- 附一个现实当中接口的开发的方式

你可以总是从REST开始,如果你要开发的东西能被自然而然的想成是一个资源。然后通过相关的工具自动生成一些代码,把这个原型和你的合作者讨论一下。这是我能想到的REST能做的一件很好的事情——快速实验。

然后如果你想认真的往下做,就可以彻底忘记REST这件事。开始自己定义业务接口,尽量不要在url里加变量。尽量只用GET和POST,减少一些沟通上的混乱。对于每个接口,好好定义可能发生的业务错误,并与PM一起协商怎么处理这些错误。认真的考虑认证、授权、流控等机制,当你开发的是和钱相关的业务尤其要留意。

最后,本文并不是说“绝对不要用REST”,而是:如果你在实际工作中用REST有了困惑,不知道某个情况下REST此时的最佳实践是什么时,不要追求“真正的REST会怎么做”,不要被REST限制住。

----- 2018-12-19更新

感谢 @David Dong 的评论,基于他的评论,我写下我的感受。

如果看过REST最初的那篇论文Architectural Styles and the Design of Network-based Software Architectures就会发现,当时想设计的目标是解决互联网级别的信息共享和互操作问题。而我们的大量开发者工作的主要目标是“为业务系统实现一个满足功能(比如登录,交易……)/非功能需求(比如认证,性能,可扩展性……)的接口“。并且设计接口时会区分“给第三方用的开放接口”、“给UI开发定制的接口“和“内部使用的接口”等。这些接口的设计目标都和REST当初制定的目标有差别。其中最接近的,是“开放接口”。因此可以看到有些开放接口用REST实现还是很不错的,比如github的接口,AWS S3的接口等。

但是其他两类接口与REST关注的点完全不一样。比如面向UI的接口的就要满足UI需要。此时资源不资源不太重要,而是尽量用少的roundtrip去返回这个界面需要的所有数据。接口是按照加载的优先级,而不是“资源”做切分。比如第一屏的显示要尽量一个接口先给出来,后续异步加载的数据可以用其他接口慢慢出。为UI提供的接口往往被划归为“大前端“的一部分。

而内部的接口,越接近DB的,越容易用表来mapping到“资源“,但是内部的接口需要考虑到数据整合的需要。比如底层的用户数据分为A、B、C三类,但这3个数据因为服务隔离不能直接在DB做join。需求要按照A的某个条件做排序分页,但要注入B和C的数据。这时就需要B和C提供batch读取和app注入的相关逻辑。此外还有复杂的查询条件,可动态改变的输出字段等要求。REST的“资源”概念在这里能帮上的忙有限。这也是GrpahQL尝试解决的问题。

再有一类问题是用接口实现分布式一致性的业务问题。比如下单+支付+扣库存+加积分问题。这时接口的形式并不重要,而能够支持实现SAGA或者TCC才是最关键的。而整个业务对外的感觉实际上是创建一个“事务”。早期一本叫做Resftul Web Services的书描述Restful接口做这个事情的方案是:

  1. 调用接口创建一个事务的资源
  2. 拿着事务资源的id,调用步骤1接口,步骤2接口……
  3. 拿着事务资源的id,调用事务的commit接口

这种形式不仅臃肿,还把怎么做这件事的内部细节完全暴露到了调用方,造成了耦合。而我们一般常见的做法就是一个接口POST /doSomething,然后接口实现方内部维护事务,维护commit,rollback等细节。有的时候还需要添加一些异步回调。

简单总结下,写接口的目标各自不同。而REST的目标是“实现互联网级别的信息共享系统”,这个目标和大部分开发者要实现的目标完全不同,这就不难解释为何照搬REST去做另一个领域的事情可能会非常别扭。

两者没有高下之分,无非是一种约定俗成的标准。习惯用RPC就用RPC,能理解REST就用REST。

JSON-RPC比较符合直观,格式也相对宽松;

REST最近正流行,有自己的一套设计规范。


REST面对的疑问跟当年刚开始流行面向对象时的情况是一样的。

它适合很多情况,但并不适合所有情况。

最差的结果就是盲目跟风,又对REST的概念和理念一知半解,最后搞出一个半吊子的怪胎,还自我标榜用了流行的RESTful API。


REST是一种设计风格,它的很多思维方式与RPC是完全冲突的。

RPC的思想是把本地函数映射到API,也就是说一个API对应的是一个function,我本地有一个getAllUsers,远程也能通过某种约定的协议来调用这个getAllUsers。至于这个协议是Socket、是HTTP还是别的什么并不重要;

RPC中的主体都是动作,是个动词,表示我要做什么。

而REST则不然,它的URL主体是资源,是个名词。而且也仅支持HTTP协议,规定了使用HTTP Method表达本次要做的动作,类型一般也不超过那四五种。这些动作表达了对资源仅有的几种转化方式。


这种设计思路是反程序员直觉的,因为在本地业务代码中仍然是一个个的函数,是动作,但表现在接口形式上则完全是资源的形式。

就像面向对象的「万物皆对象」理论在习惯了纯粹面向过程开发的程序员眼里显得十分别扭一样:我的代码本来就是按顺序、循环、分支这么运行的啊,为啥非得在很明确的结构上封装一层一层的基类子类接口,还要故意给两个函数起同一个名字,调用时才选择用哪一个呢?


使用「万物皆资源」的思想编写实际项目中的API接口时,最常见的问题就是「这玩意到底是个什么资源?………………算了,我就直接写吧,不管什么风格了」

  • 比如,login和logout应该怎么REST化?
  • 比如,多条件复合搜索在GET里写不下怎么办?
  • 比如,大量资源的删除难道要写几千个DELETE?

其实在理解了REST后,这些都不是什么无解的难题,只是思维方式要转换一下:

  • login和logout其实只是对session资源的创建和删除;
  • search本身就是个资源,使用POST创建,如果不需持久化,可以直接在Response中返回结果,如果需要(如翻页、长期缓存等),直接保存搜索结果并303跳转到资源地址就行了;
  • id多到连url都写不下的请求,应该创建task,用GET返回task状态甚至执行进度;

……等等等。


如果只是规定了一种规范,却不理解它表相下面的思维方式,实施中又按照自己的理解随意变动,那结果肯定是混乱不堪的。

当然,API怎么写是开发者的自由。但如果一个API在url里放一堆动词、资源设计混乱、各种乱用HTTP Method和Status Code,还自称RESTful API的话,那就像你养了一条狗,还管它叫猫一样。

这种混搭产物,不如叫它REFU吧。

(Remove Extension From Url:从url里去掉文件扩展名)


前面说了半天REST的理念和不懂REST造成的问题,但是,这并不代表REST比RPC更「高等」,更不是说不理解REST的人是落伍的。

所谓代码风格、接口形式、各种林林总总的格式规定,其实都是为了在团队内部形成共识、防止个人习惯差异引起的混乱。JSON-RPC当然也是有规范的,但相比REST实在宽松太多了。

如果一个开发团队规定必须在url里写action,所有请求都是POST,可以吗?当然也没问题,只是不要拿出去标榜自己写的是RESTful API就行。

规范最终还是为了开发者和软件产品服务的,如果它能带来便利、减少混乱,就值得用;反之,如果带来的麻烦比解决的还多,那就犯不上纯粹跟风追流行了。