正确打开REST
经历了无数渣渣项目过后,才彻底理解REST(Restful Web Service,下统称REST)的一些真谛,但即使如此,我的理解也只是一家之言,没有蔑视谁的意思,只是希望作为一个引渡者,让读者对REST有真正的感悟。当有一天我们发现我们曾经一度使用的REST到最后都偏离了初衷,那个时候我们会明白自己手中的应用隔真正的REST还任重道远,我们需要REST,更需要理解REST并且驾驭它。
REST不是一种标准,只是Web服务中的一种架构指导风格。
这也是REST的难点,由于它不像XML下的SOAP一样是一种正统的规范,所以很多人一直在设计REST类型的应用时,没有一个固定的风格以及最佳实践,我不期望你马上找到答案,但是在本章结束后,希望你心中已经有答案。
1. REST/SOAP
本文不打算介绍SOAP的基础知识,大家有兴趣可以去度娘,主要是对比一下这两种Web服务,让读者俯瞰REST所处的位置,当然之初我们还是要扒扒一些简单的故事背景,否则读者会觉得不完整。
Web Service又称为Web服务,它具有平台独立性、低耦合性、自包含性,最初开发的Web服务应用,直接使用了开放的XML标准来描述、发布、发现、协调、配置各种组件,用于开发可交互的分布式应用程序——它不依赖任何第三方语言平台,定义了统一的标准。今时今日一般实现Web服务类应用会使用两种风格:SOAP风格和REST风格。
SOAP全称为Simple Object Access Protocol——简单对象访问协议,它是用来描述Web服务的消息格式的一种规范,基于XML标准定制,在这种Web服务中,所有通信的消息格式都是XML的。
REST全称为Representational State Transfer——表述性状态传递,它是Roy Fielding博士在2000年中的博士论文中提出来的一种软件架构风格,相比于SOAP,它可以降低开发的复杂度,提高系统的可伸缩性。
在对比二者之前先看看下图:
上图算是真正诠释了REST和SOAP的本质区别,不单单从数据格式上来区分。SOAP本身支持XML格式,而REST既支持XML格式又支持JSON格式(不要盲目觉得JSON格式的才是REST),但二者真正的区别在“语义”上。REST风格的Web服务是一种新的Web服务,由于它本身不是标准(SOAP是一种标准),所以一直缺乏在架构时所谓的最佳实践,但是Richardson成熟度模型给了我们一个很好的启示【Reference】。
SOAP和REST真正意义上的区别在于“协议语义”上,计算机网络中的HTTP协议是应用层协议,设计它的最初就是定义面向应用的标准网络协议,不知道读者有没有发现SOAP协议却是“背道而驰”的,因为它在HTTP协议之上重新封装了一层,如Envelop,SOAPBody,SOAPHeader
,在这样的条件下,HTTP协议在SOAP风格的Web服务中,实际上只是做了“传输”的作用,“应用”的语义被SOAP协议本身取而代之。相信开发过SOAP Web服务的开发人员对SOAP中定义错误信息并不陌生,主要是依赖SOAPHeader中的语义。
REST的流行并不是空穴来风,从图上可以知道,REST的本质就是“释放HTTP协议”,在这种风格的Web服务中,它更提倡围绕HTTP协议本身的东西来设计,而不是重新封装。所以最初的REST设计会让很多人不习惯,主要是因为它需要开发人员深度理解HTTP协议,而不是REST,在进入REST设计之前,大家可以看看下边这些常见的场景,相信对很多人而言都不陌生,也是在Web服务设计中遗留下来的一些“小瑕疵”——是的,最初准备使用的词是“诟病”,但仔细想想,不能为了REST而去REST,所以姑且定义为“小瑕疵”,我们设计系统的目的是投入到使用,并不是为了往规范靠近,往规范靠近往往只是一种“推荐”,它的好处是提高你代码的生命力。
- 很多应用由于习惯了SOAP协议,所以使用HTTP协议时通常只会用到GET,POST两种HTTP方法,这种设计成为了曾经某一个时代SOA面向服务架构中的一种主流。
- 对很多开发人员而言,就像8/2法则一样,使用HTTP状态代码只开了像404、401、403、200、500这种高频使用的状态代码,而对于HTTP中的其他状态代码,大部分人可能都不去关注——对的,SOAP中有SOAPHeader中的Server Error/Client Error,所以这种做法对很多工程师而言是一种历史遗留原因。
- 不注重内容协商:由于很多人将内容协商全部交给了Web容器,而容器本身是从技术的角度去考虑,而不是业务,使得大部分的REST应用仅处理了诸如
application/json
类型的内容协商处理,大家忘记了Web服务系统中最本质的:序列化子系统。- 不开放路径参数:曾经有一个人告诉我,路径参数会损害性能,所以将禁用路径参数写入到了提纲中。其实对我这种无信仰的软件工程师而言,当时只是觉得:井蛙不可以语于海者,拘于虚也;夏虫不可以语于冰者,笃于时也。因为我所关心的是功能先于性能,约定先于规范的设计,不过也有不使用路径参数的很好的案例,但别忘了人家是投入了大量精力在文档层面和规范定义中的。
客观来讲,SOAP和REST本质的区别在于二者对”HTTP协议“的态度,SOAP在"HTTP协议”中重新封装了一层,把它当成了纯粹的传输层来使用,但你不能说SOAP设计不好,充其量只能说它将“HTTP应用层协议”的语义进行了稍许束缚。而REST在Richardson成熟度模型中,被定义成完全和HTTP协议融合的标准Web服务规范,REST的成熟度越高,它对HTTP协议本身驾驭得更好,这种方式下设计出来的REST相对而言会“有章可循”,同时这种REST会很大力度地去释放“HTTP协议”的语义。
本小结最后,补充一个SOAP和REST的全方位比较表格:
主题 | SOAP | REST |
---|---|---|
起源 | SOAP是1998年由Dave Winer等人与微软合作时提出的,该协议由大型软件公司开发,目的在于满足企业级市场服务需求。 | REST则是由加州大学尔湾分院的Roy Fielding于2000年提出的,这个概念诞生于学术环境,涵盖了开放式网络的概念。 |
基本概念 | 使数据可用作服务(动词+名词),例如getUser或PayInvoice。 | 使数据可用于资源(名词),比如user或invoice。 |
优点 | 1)SOAP遵循正式的企业规范;2)可在任何通讯协议上运行,甚至是异步方式;3)关于对象的信息需要传递给客户;4)安全性和授权也属于SOAP协议的一部分;5)可使用WSDL语言进行完全描述。 | 1)REST遵循开放式网络理念;2)容易实施和维护;3)明确分离客户端和服务端的实现;4)通信不由单个实体控制;5)客户端可存储信息,以防止多次调用;6)REST可用多种格式返回数据(如XML、JSON) |
缺点 | SOAP华妃大量带宽来传递数据;SOAP实现比较困难,在Web和移动应用开发人员中不太受欢迎。 | REST仅在HTTP协议之上运行;很难仅在REST之上执行授权和安全性。 |
适用场景 | 客户端需要访问服务器上可用的对象;在客户端和服务端之间执行正式的协议。 | 客户端和服务器在Web环境中运行;关于对象的信息不需要传递到客户端。 |
不适用的场景 | 如果广大开发人员希望能够轻松使用API,SOAP不太容易满足;SOAP在带宽非常有限的场景中也不太适用。 | 需要在客户端和服务器之间执行严格的协议时,REST不适用;执行涉及多个调用的事务时,REST也不适用。 |
使用实例 | 金融服务、支付网关、电信业务 | 社交媒体服务、社交网络、网络聊天服务、移动网络 |
小结 | 如果正确处理事务性操作,并且已经有对SOAP技术满意的受众群体,可使用SOAP。 | 如果专注于大规模采用API或您的API针对移动应用,请使用REST。 |
2. REST设计要素
其实最初在写论文时由于课题方向问题,没有写入这部分,但通过研究和实践,这里我会尝试用自己的方式去解读Richardson成熟度模型,提供一个REST风格的Web服务设计的基本要素——回到最初,学个Vert.x有必要么?没必要,真没必要,可“工欲善其事必先利其器”,如果您在将来很长一段实践都会去使用vertx-web
子项目开发Web服务、微服务、或者之后的Service Mesh,那么我觉得理解这些基础理论是有必要的,毕竟它会让你在系统架构中少踩很多大坑,zero实际上就是基于JSR规范设计的面向REST风格的Web服务/微服务框架。
2.1.REST和HTTP
高山流水觅知音,既然谈到了“HTTP协议”,那么我们不得不曲径通幽,再一次去瞅瞅REST和HTTP协议的关系,只是这一次我们从现象上去解读一些基本心得。
- REST的全称是
Representational State Transfer
,它是面向资源的一种Web服务,其实它有一个最大的特征就是客户端和服务端在请求交互过程是无状态的——那么"HTTP协议"就是它天生的土壤。 - REST从章节一种的图可以看到,几乎在“应用层”和“HTTP协议”实现了绑定,这种绑定不仅仅是大力度使用HTTP方法(在设计过程中启用DELETE,PUT、PATCH等),还更大力度启用了HTTP状态代码(如406、410、201、415、449等),它和“HTTP协议”有一种我中有你,你中有我的感觉。
- 容易被开发工程师忽略的一个REST设计原则,就是实现幂等性(Idempotence),而幂等性是HTTP协议天生绑定的一种特性,就像写出全函数的方法很难一样,要让您所有设计的REST的API都具备幂等性,几乎有点“极限理论”的感觉——我们只能尽可能让每个API都具有幂等性,但不保证所有的API都具有幂等性。
- 资源设计,REST本身是面向资源的架构指导风格,所以更多的时候需要您在资源这一“术语“上去考虑(其实使用路径参数我个人看来主要是去实现“资源规约”,将某一类的资源按照“小法则”管理起来),这一“术语“包括资源地址、资源名称、资源快照、资源内容、资源格式等。
- 内容协商,虽然不可规避的我们会大规模使用
application/json
这种类型的Web服务,但总有20%
左右的领域里,这样的媒体类型是不适用的,在此种情况下,我们还是需要系统本身的内容协商处理变得更容易扩展,而不是每次遇到了一种新的媒体类型都需要通过很大力度地”手术“,才在系统中可添加组件来处理。
如果您设计的系统参考了上边的基本现象,那么从我个人的角度,恭喜你,可能这个时候你对”HTTP协议“的应用也在往深处走,因为这样的系统中的REST风格的Web服务,绝对会很大力度上去发掘”HTTP协议“的语义,而不是浅尝辄止。
我写这个心得的目的不是告诉你REST的应用应该如此设计,只是我个人在设计这种类型的Web服务系统时由于后期需求扩展确实踩到了不少相关的坑,回过头来看Richardson成熟度模型以及更多参考文献时,看到大部分REST风格的系统在开始往这个方向去转变——告诉读者这么多,也不是让你突然脑袋发热,作出错误决定:来,按照这个这种规则推翻重来!
2.2.再谈幂等性
【Reference】回到核心原则,我们再谈谈HTTP协议的幂等性(Idempotence),是的,HTTP协议中的幂等性是这样定义的:
Methods can also have the property of "idempotence" in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request.
读者需要知道的是幂等性本身不是为了REST的Web服务设计的,而是为了分布式计算而设计的,由于微服务的影响,REST的Web服务在互联网环境备受欢迎,越来越多的系统都是基于REST的Web服务设计的API,而这些API都是运行在HTTP协议之上的。幂等性定义了这样一种性质:针对某一个API,一次请求和多次请求应该具有同样的副作用,它和编译器的做法有点类似:不论请求多少字,产生的最终效果应该具有绝对一致性(这种一致性包括数据库更改、网络请求、第三方接口影响、文件系统访问等);HTTP规范本身没办法通过消息格式这种语法手段来定义它,所以这种特性一直不被重视——但是大多数工程师忘了,幂等性是分布式系统设计中很重要的一个概念。
关于分布式设计和幂等性设计的对比,可以参考引用中的例子,一旦提到幂等性设计自然还会关联到安全性,HTTP协议中的幂等性和安全性的表格如下:
方法名 | 安全性 | 幂等性 |
---|---|---|
GET | 是 | 是 |
HEAD | 是 | 是 |
OPTIONS | 是 | 是 |
DELETE | 否 | 是 |
PUT | 否 | 是 |
POST | 否 | 否 |
HTTP协议设计的初衷就是面向资源的“应用层协议”,但它的使用在实际上存在了分歧,就是前文提到的:SOAP风格和REST风格——这里我们再回顾一下前文提到的REST和SOAP的区别。
- REST风格:它将HTTP协议当成了应用层协议,忠实地遵循了HTTP协议的规定;
- SOAP风格:它没有完全把HTTP当成应用层协议,而是做了“传输”作用,然后在HTTP之上又封装了一层,建立了自己的应用层协议。
基本上到这里我相信读者对SOAP/REST的区别就再也没有什么歧义了,接下来针对上述表格逐一解读一下,让读者对幂等性有一定的了解,这些内容大部分摘录于引用文,当然也会有我自己的理解。
这里仅仅介绍常用的四种方法:GET/POST/DELETE/PUT
,读者如果理解了HTTP方法本身具有的特性,那么对于设计REST风格的Web服务就会更加得心应手,看看Todd Wei的基本描述:
- GET方法用于获取资源,天生的无副作用,所以GET方法本身是幂等的。比如请求:
GET http://www.bank.com/account/123456
,该请求不会改变资源状态,调用一次和调用N次的结果相同,所以是没有副作用的。——这里Todd Wei也强调了,这里调用N次不影响不是说每次GET的数据结果相同(123456这条记录的数据有可能更新过)。 - DELETE方法用于删除资源,它本身具有副作用,但是它可以满足幂等性。比如:针对请求:
DELETE http://www.bank.com/account/123456
,调用一次和调用N次应该产生同样的副作用,也就是说删除123456这条账号记录时,不论发多少次请求都应该有个正确的结果回来,防止客户端重复请求带来的副作用。 - POST和PUT:这两种方法容易简单被定义成“POST表示创建资源,PUT表示更新资源”(其实我自己以前都一直这样用),但是实际上,二者都可以用于创建资源,本质区别就是在幂等性上。
先看看POST和PUT在HTTP规范中的原始定义:
The POST method is used to request that the origin server accept the entity enclosed in the request as a new subordinate of the resource identified by the Request-URI in the Request-Line ...... If a resource has been created on the origin server, the response SHOULD be 201 (Created) and contain an entity which describes the status of the request and refers to the new resource, and a Location header.
The PUT method requests that the enclosed entity be stored under the supplied Request-URI. If the Request-URI refers to an already existing resource, the enclosed entity SHOULD be considered as a modified version of the one residing on the origin server. If the Request-URI does not point to an existing resource, and that URI is capable of being defined as a new resource by the requesting user agent, the origin server can create the resource with that URI.
其实POST对应的URI并非创建的资源本身,而是这个资源的接受者,如http://www.forum.com/articles
的语义是创建下一篇帖子,响应中应该包含创建状态、以及创建好的读取帖子的URI;如果针对同一个请求发两次,那么相同的POST请求应该在服务端创建两份资源,并且具有不同的URI——因此POST不具有幂等性。PUT则不一样,她属于Save
级别,如果针对同一个URI发送请求如:http://www.forum.com/articles/123
的语义是创建/更新ID为123的帖子,针对同一个URI调用多次PUT其副作用是相同的,所以PUT方法本身具有幂等性。
2.3.幂等性设计
如果只是简单引用Todd Wei的幂等性相关法则,那么写这本书似乎就变得没有意义了,关于幂等性在实际项目中的设计,是一个难点,所以这里谈一些相关设计的基础规则。
- DELETE——也许最直接的幂等性设计在于DELETE方法,在传统的很多应用里,如果针对DELETE执行第二次调用,要么我们就返回200告诉客户端什么也没发生,要么就直接在后端报错,来个
500
的大餐(其实就是没有开发第二次的删除法则),实际上在我开发zero的应用时,更加提倡的是410 Gone
这个代码,之所以这样设计是在于410
不同于404
,实际上删除的基础流程为:查找旧记录 -> 删除旧记录 -> 返回成功,对于第二次删除在查找旧记录时就会遇到问题,所以这个时候使用410
或者404
是一种不错的做法。——所以即使服务端什么也没发生,也需要提供给客户端一个“永恒态”,这样的方式会使得在N次调用发生时总是返回同一个结果。 - POST——POST的第一个用途,毫无疑问就是添加,但在HTTP协议中,添加是有“双态”的,所以设计POST的添加接口时需要考虑两种情况:
- 请求数据中包含Unique:如果您请求的数据中包含了Unique的键,那么这种情况下,同时添加两次会遇到数据库底层的Duplicated的错,这种所以这种情况下最好的办法是第一次添加返回
200 OK
,第二次(以及N次)添加同一条记录返回201 Created
的状态(记得在响应中带上Location头)。 - 请求数据不包含Unique:这种情况下,对不起,由于POST方法天生不具备幂等性法则,那么这样的数据进入到服务端过后,只能做二次数据添加,折中一点的操作就是设置一个时间间隔,在这个时间间隔内不让客户端重复提交。
- 请求数据中包含Unique:如果您请求的数据中包含了Unique的键,那么这种情况下,同时添加两次会遇到数据库底层的Duplicated的错,这种所以这种情况下最好的办法是第一次添加返回
- PUT/POST——当PUT和POST同时被用于更新时,其实我更推荐使用PUT更新,这样做可能有驳于HTTP协议本身的“语义”,但是PUT方法天生的幂等性,那么在后端逻辑处理时可以更加自由,且不用去考虑一些数据一致性的问题,而在更新过程中一般有一种情况还是提倡使用POST更新,就是标准CRUD的U发生的时候(全量更新)。
- POST查询——这是一个极其老生常谈的话题,很多时候在处理复杂查询、搜索的时候,由于参数结构复杂,通常会采用POST方法替换GET方法来写查询接口,这种情况下POST方法就不能享受GET查询在客户端缓存带来的福利(默认不支持),但却更符合大部分业务场景的设计,这种方式下由于Cache-Control和Expires头会没有作用,所以在处理缓存时候只能把压力递交给服务端,让服务端将频繁查询存储起来,使用“查询存储”的机制。
- 禁用GET写——很多人在设计REST时为了图方便,有时候会使用GET方法调用后端接口并且执行写操作,如添加、删除、修改,如果使用了写操作,那么GET方法的接口可能破坏了HTTP的幂等性,它的每次读取对后端的数据以及状态产生了影响,并且每次返回结果都不一样。
上述的项只是在设计REST过程中的一些基本的心得,并不是最佳实践,归根到底,最终如何设计REST要根据实际的业务情况来,您在设计REST时可以在“规范”和“需求”之间找到一个这种,而HTTP协议的安全和幂等性可以作为您设计的接口的一个标准,来衡量该接口是否具有了幂等性。
2.4.条件请求
如果只是简单的REST设计,大部分的开发人员都不会陌生,而往往比较陌生的是条件请求(Conditional Request),该设计主要处理两个问题:
- 对于GET请求,条件请求用于帮助客户端和缓存校验被缓存的数据是否最新的。
- 对于非安全请求(POST、PUT、DELETE),条件请求可以提供并发控制的功能。
并发控制也是REST设计中的一个难点,如果您设计的GET、POST、PUT、DELETE相关的API不支持并发控制,那么它将影响到应用本身的完整性以及数据的一致性,导致服务端出现“更新丢失”或“删除过期”等问题。注意:这里的“删除过期”和前文提到的410
的返回结果不一样,这里是当多个请求同时发生时,删除还没执行完,而其他请求已经盯上了该资源导致“删除不成功”,和已经删除过后再提交删除请求是两个概念,读者一定要区分。关于条件请求的并发控制主要包含两种方式:
- 悲观:客户端会得到一个锁,获得当前资源状态,并且修改,然后将它释放。这个过程中,服务端禁止客户端得到同一资源的锁,关系数据库就是按照该模型运作的。
- 乐观:客户端会得到一个令牌,并将该令牌包含到请求中尝试些,而不是去获取锁——一旦令牌有效,那么操作成功,否则就失败(HTTP协议之上的REST就是这种方式)。
谈了这些内容后,相信读者会对REST本身的设计有些畏惧了,实际上完全不用担心,大家可以参考《RESTful Web Service Cookbook》这本书去阅读REST的相关细节,真正设计出规范而又能满足需求的REST相关的API。
3.总结
不得不承认Vert.x本身并不是为了REST量身打造的框架,但它也有很多可以让Vert.x在REST中流畅运行的子项目,这也是本章的目的,着眼于vertx-web
这个项目去开发真正意义上的REST应用。当然我尝试着在这个过程中去介绍一些和REST以及后期使用的微服务相关的内容,比如vertx-api-contract
以及vertx-service-discovery
和vertx-circuit-breaker
等,希望接下来带给读者的旅程是有意义的。同样希望读者在阅读了本章内容后,才真正意义上对REST有一个了解,既然它是一种架构指导风格,那么在设计REST接口时就不是一个0和1的问题,没有对错,您只是需要寻找对你而言实用性最强切最有意义的REST设计。