著者前言
很多人会拿REST与RPC相比较,其实,REST无论是在思想上、在概念上,还是在使用范围上,与RPC都不尽相同,充其量只能算是有一些相似,应用会有一部分重合之处,但本质上并不是同一类型的东西。
REST与RPC在思想上差异的核心是抽象的目标不一样,即面向过程的编程思想与面向资源的编程思想两者之间的区别。
REST与RPC在概念上的不同是指REST并不是一种远程服务调用协议,甚至可以把定语也去掉,它就不是一种协议。
常有人批评某个系统接口“设计得不够RESTful”,其实这句话本身就有些争议,REST只能说是风格而不是规范、协议,并且能完全符合REST所有指导原则的系统也是不多见的。
REST的历史和定义
REST源于Roy Thomas Fielding在2000年发表的博士论文“Architectural Styles and the Design of Network-based Software Architectures”
笔者比较推荐先理解什么是HTTP,再配合一些实际例子来对两者进行类比,以更清楚地了解REST,你会发现REST实际上是“HTT”(Hypertext Transfer)的进一步抽象,两者的关系就如同接口与实现类的关系一般。
REST的定义
REST(Representational State Transfer,表征状态转移)
资源(Resource):譬如你现在正在阅读一篇名为《REST设计风格》的文章,这篇文章的内容本身(你可以将其理解为蕴含的信息、数据)称之为“资源”。无论你是通过阅读购买的图书、浏览器上的网页还是打印出来的文稿,无论是在电脑屏幕上阅读还是在手机上阅读,尽管呈现的样子各不相同,但其中的信息是不变的,你所阅读的仍是同一份“资源”。
·表征(Representation):当你通过浏览器阅读此文章时,浏览器会向服务端发出“我需要这个资源的HTML格式”的请求,服务端向浏览器返回的这个HTML就被称为“表征”,你也可以通过其他方式拿到本文的PDF、Markdown、RSS等其他形式的版本,它们同样是一个资源的多种表征。可见“表征”是指信息与用户交互时的表示形式,这与我们软件分层架构中常说的“表示层”(Presentation Layer)的语义其实是一致的。
·状态(State):当你读完了这篇文章,想看后面是什么内容时,你向服务端发出“给我下一篇文章”的请求。但是“下一篇”是个相对概念,必须依赖“当前你正在阅读的文章是哪一篇”才能正确回应,这类在特定语境中才能产生的上下文信息被称为“状态”。我们所说的有状态(Stateful)抑或是无状态(Stateless),都是只相对于服务端来说的,服务端要完成“取下一篇”的请求,要么自己记住用户的状态,如这个用户现在阅读的是哪一篇文章,这称为有状态;要么由客户端来记住状态,在请求的时候明确告诉服务端,如我正在阅读某某文章,现在要读它的下一篇,这称为无状态。
·转移(Transfer):无论状态是由服务端还是由客户端来提供,“取下一篇文章”这个行为逻辑只能由服务端来提供,因为只有服务端拥有该资源及其表征形式。服务端通过某种方式,把“用户当前阅读的文章”转变成“下一篇文章”,这就被称为“表征状态转移”。
通过“阅读文章”这个例子,相信你应该能够理解“表征状态转移”的含义了。借着这个故事的上下文状态,笔者再继续介绍几个现在不涉及但稍后要用到的概念。
·统一接口(Uniform Interface):上面说的服务端“通过某种方式”让表征状态转移,那具体是什么方式呢?如果你真的是用浏览器阅读本文电子版的话,请把本文滚动到结尾处,右下角有下一篇文章的URI超链接地址,这是服务端渲染这篇文章时就预置好的,点击它让页面跳转到下一篇,就是所谓“某种方式”的其中一种方式。任何人都不会对点击超链接网页出现跳转感到奇怪,但你细想一下,URI的含义是统一资源标识符,是一个名词,如何能表达出“转移”动作的含义呢?答案是HTTP协议中已经提前约定好了一套“统一接口”,它包括GET、HEAD、POST、PUT、DELETE、TRACE、OPTIONS七种基本操作,任何一个支持HTTP协议的服务器都会遵守这套规定,对特定的URI采取这些操作,服务器就会触发相应的表征状态转移。
·超文本驱动(Hypertext Driven):尽管表征状态转移是由浏览器主动向服务器发出请求所引发的,该请求导致了“在浏览器屏幕上显示出了下一篇文章的内容”的结果。但是,我们都清楚这不可能真的是浏览器的主动意图,浏览器是根据用户输入的URI地址来找到网站首页,读取服务器给予的首页超文本内容后,浏览器再通过超文本内部的链接来导航到这篇文章,阅读结束时,也是通过超文本内部的链接再导航到下一篇。浏览器作为所有网站的通用的客户端,任何网站的导航(状态转移)行为都不可能是预置于浏览器代码之中,而是由服务器发出的请求响应信息(超文本)来驱动的。这点与其他带有客户端的软件有十分本质的区别,在那些软件中,业务逻辑往往是预置于程序代码之中的,有专门的页面控制器(无论在服务端还是在客户端中)来驱动页面的状态转移。
·自描述消息(Self-Descriptive Message):由于资源的表征可能存在多种不同形态,在消息中应当有明确的信息来告知客户端该消息的类型以及应如何处理这条消息。一种被广泛采用的自描述方法是在名为“Content-Type”的HTTP Header中标识出互联网媒体类型(MIME type),譬如“Content-Type:application/json;charset=utf-8”说明该资源会以JSON的格式来返回,请使用UTF-8字符集进行处理。
REST的原则
Fielding认为,一套理想的、完全满足REST风格的系统应该满足以下六大原则。
1.客户端与服务端分离(Client-Server)
2.无状态(Stateless)
3.可缓存(Cacheability)
4.分层系统(Layered System)
5.统一接口(Uniform Interface)
下面以一个例子来说明:譬如,对于几乎每个系统都有的登录和注销功能,如果你理解成登录对应于login()服务,注销对应于logout()服务这样两个独立服务,这是“符合人类思维”的;如果你理解成登录是PUT Session,注销是DELETE Session,这样你只需要设计一种“Session资源”即可满足需求,甚至以后对Session的其他需求,如查询登录用户的信息,就是GET Session而已,其他操作如修改用户信息等也都可以被这同一套设计囊括在内,这便是“抽象程度更高”带来的好处。
6.按需代码(Code-On-Demand)
RMM
RESTful Web APIs和RESTful Web Services的作者Leonard Richardson曾提出一个衡量“服务有多么REST”的Richardson成熟度模型(Richardson Maturity Model,RMM),以便让那些原本不使用REST的系统,能够逐步地导入REST。Richardson将服务接口“REST的程度”从低到高,分为0至3级。
·第0级(The Swamp of Plain Old XML):完全不REST。
·第1级(Resources):开始引入资源的概念。
·第2级(HTTP Verbs):引入统一接口,映射到HTTP协议的方法上。
·第3级(Hypermedia Controls):超媒体控制,在本文里面的说法是“超文本驱动”,在Fielding论文里的说法是“Hypertext As The Engine Of Application State,HATEOAS”,其实都是指同一件事情。
demo案例
医生预约系统
作为一名病人,我想要从系统中得知指定日期内我熟悉的医生是否具有空闲时间,以便于我向该医生预约就诊。
第0级医院开放了一个/appointmentService的Web API,传入日期、医生姓名等参数,可以得到该时间段内该名医生的空闲时间,该API的一次HTTP调用如下所示:
1 POST /appointmentService?action=query HTTP/1.1 2 3 {date: "2020-03-04", doctor: "mjones"}
然后服务器会传回一个包含了所需信息的回应:
1 HTTP/1.1 200 OK 2 3 [ 4 {start:"14:00", end: "14:50", doctor: "mjones"}, 5 {start:"16:00", end: "16:50", doctor: "mjones"} 6 ]
得到了医生空闲的结果后,笔者觉得14:00比较合适,于是进行预约确认,并提交了个人基本信息:
1 POST /appointmentService?action=comfirm HTTP/1.1 2 3 { 4 appointment: {date: "2020-03-04", start:"14:00", doctor: "mjones"}, 5 patient: {name: icyfenix, age: 30, ……} 6 }
如果预约成功,那我能够收到一个预约成功的响应:
1 HTTP/1.1 200 OK 2 3 { 4 code: 0, 5 message: "Successful confirmation of appointment" 6 }
如果出现问题,譬如有人在我前面抢先预约了,那么我会在响应中收到某种错误消息:
1 HTTP/1.1 200 OK 2 3 { 4 code: 1 5 message: "doctor not available" 6 }
至此,整个预约服务宣告完成,直接明了,我们采用的是非常直观的基于RPC风格的服务设计,似乎很容易就解决了所有问题,但真的是这样吗?
第1级
第0级是RPC的风格,如果需求永远不会变化,那它完全可以良好地工作下去。但是,如果你不想为预约医生之外的其他操作、为获取空闲时间之外的其他信息去编写额外的方法,或者改动现有方法的接口,那还是应该考虑一下如何使用REST来抽象资源。
通往REST的第一步是引入资源的概念,在API中的基本体现是围绕资源而不是过程来设计服务,说得直白一点,可以理解为服务的Endpoint应该是一个名词而不是动词。此外,每次请求中都应包含资源的ID,所有操作均通过资源ID来进行,譬如,获取医生指定时间的空闲档期:1 POST /doctors/mjones HTTP/1.1 2 3 {date: "2020-03-04"}
然后服务器传回一组包含了ID信息的档期清单,注意,ID是资源的唯一编号,有ID即代表“医生的档期”被视为一种资源:
1 HTTP/1.1 200 OK 2 3 [ 4 {id: 1234, start:"14:00", end: "14:50", doctor: "mjones"}, 5 {id: 5678, start:"16:00", end: "16:50", doctor: "mjones"} 6 ]
笔者还是觉得14:00的时间比较合适,于是又进行预约确认,并提交了个人基本信息:
1 POST /schedules/1234 HTTP/1.1 2 3 {name: icyfenix, age: 30, ……}
后面预约成功或者失败的响应消息在这个级别里面与之前一致,就不重复了。
比起第0级,第1级的特征是引入了资源,通过资源ID作为主要线索与服务交互,但第1级至少还有三个问题没有解决:
一是只处理了查询和预约,如果临时想换个时间,要调整预约,或者病忽然好了,想删除预约,这都需要提供新的服务接口;
二是处理结果响应时,只能依靠结果中的code、message这些字段做分支判断,每一套服务都要设计可能发生错误的code,这很难考虑全面,而且也不利于对某些通用的错误做统一处理;
三是没有考虑认证授权等安全方面的内容,譬如要求只有登录用户才允许查询医生档期时间,某些医生可能只对VIP开放,需要特定级别的病人才能预约,等等。
第2级
第1级遗留的三个问题都可以通过引入统一接口来解决。HTTP协议的七个标准方法是经过精心设计的,只要架构师的抽象能力够用,它们几乎能涵盖资源可能遇到的所有操作场景。
REST的具体做法是:把不同业务需求抽象为对资源的增加、修改、删除等操作来解决第一个问题;
使用HTTP协议的Status Code,它可以涵盖大多数资源操作可能出现的异常,也可以自定义扩展,以此解决第二个问题;
依靠HTTP Header中携带的额外认证、授权信息来解决第三个问题,这个在实战中并没有体现,后文会在5.3节中介绍相关内容。
按这个思路,获取医生档期,应采用具有查询语义的GET操作进行:
GET /doctors/mjones/schedule?date=2020-03-04&status=open HTTP/1.1
然后服务器会传回一个包含了所需信息的回应:
HTTP/1.1 200 OK [ {id: 1234, start:"14:00", end: "14:50", doctor: "mjones"}, {id: 5678, start:"16:00", end: "16:50", doctor: "mjones"} ]
笔者仍然觉得14:00的时间比较合适,于是进行预约确认,并提交了个人基本信息,用以创建预约,这是符合POST的语义的:
POST /schedules/1234 HTTP/1.1 {name: icyfenix, age: 30, ......}
如果预约成功,那笔者能够收到一个预约成功的响应:
HTTP/1.1 201 Created Successful confirmation of appointment
如果出现问题,譬如有人抢先预约了,那么笔者会在响应中收到某种错误消息:
HTTP/1.1 409 Conflict doctor not available
第3级
第2级是目前绝大多数系统所到达的REST级别,但仍不是完美的。至少还存在一个问题:你是如何知道预约mjones医生的档期是需要访问“/schedules/1234”这个服务Endpoint的?
也许你第一时间甚至无法理解为何我会有这样的疑问,这当然是程序代码写的呀!
但REST并不认同这种已烙在程序员脑海中许久的想法。
RMM中的超文本控制、Fielding论文中的HATEOAS和现在提的比较多的“超文本驱动”,所希望的是除了第一个请求是由你在浏览器地址栏输入驱动之外,其他的请求都应该能够自己描述清楚后续可能发生的状态转移,由超文本自身来驱动。
所以,当你输入了查询的指令之后:
GET /doctors/mjones/schedule?date=2020-03-04&status=open HTTP/1.1
服务器传回的响应信息应该包括诸如如何预约档期、如何了解医生信息等可能的后续操作:
HTTP/1.1 200 OK { schedules:[ { id: 1234, start:"14:00", end: "14:50", doctor: "mjones", links: [ {rel: "comfirm schedule", href: "/schedules/1234"} ] }, { id: 5678, start:"16:00", end: "16:50", doctor: "mjones", links: [ {rel: "comfirm schedule", href: "/schedules/5678"} ] } ], links: [ {rel: "doctor info", href: "/doctors/mjones/info"} ] }
如果做到了第3级REST,那服务端的API和客户端也是完全解耦的,此时如果你要调整服务数量,或者对同一个服务做API升级时将会变得非常简单。
好处、不足与争议
好处
REST提出以资源为主体的服务设计风格,可以带来不少好处。
- 降低服务接口的学习成本。
- 资源天然具有集合与层次结构。
- REST绑定于HTTP协议。
不足与争议
标签:mjones,HTTP,1.1,预约,笔记,风格,REST,资源 From: https://www.cnblogs.com/onejay/p/181121811)面向资源的编程思想只适合做CRUD,面向过程、面向对象编程才能处理真正复杂的业务逻辑。
笔者再重复一遍,面向资源的编程思想与另外两种主流编程思想只是抽象问题时所处的立场不同,只有选择不同,没有高下之分。
2)REST与HTTP完全绑定,不适合应用于要求高性能传输的场景中。
3)REST不利于事务支持。
4)REST没有传输可靠性支持。
5)REST缺乏对资源进行“部分”和“批量”处理的能力。
譬如你准备给某个用户的名字增加一个“VIP”前缀,提交一个PUT请求修改这个用户的名称即可,而你要给1000个用户加VIP前缀时,如果真的去调用1000次PUT,浏览器会回应HTTP/1.1 429 Too Many Requests。此时,你就不得不先创建一个任务资源(如名为“VIP-Modify-Task”),把1000个用户的ID交给这个任务,然后驱动任务进入执行状态。
又譬如你去网店买东西,下单、冻结库存、支付、加积分、扣减库存这一系列步骤会涉及多个资源的变化,你可能面临不得不创建一种“事务”的抽象资源,或者用某种具体的资源(譬如“结算单”)贯穿这个过程的始终,每次操作其他资源时都带着事务或者结算单的ID。HTTP协议由于本身的无状态性,会相对不适合(并非不能够)处理这类业务场景。