REST是什么
REST从2000年被Roy Fielding提出距今已有20多年,其对Web技术产生了深远的影响。REST本身并没有产生新的技术或者中间件,REST传递的是一种设计思想,其提供了一种约束原则和条件。
REST全称为Representational State Transfer,中文为表征性状态转移,感觉前面其实还少了一个主语“资源”,个人理解应该是“资源表征性状态转移”。而其核心就是通过创造一种资源的定义与描述原则,形成一种标准化规范,从而减少技术人员在开发与沟通时候的成本。
实现REST风格的框架叫Restful架构时,而我们主要是使用的HTTP作为这种规范的载体,本文也是针对HTTP的形式来进行讨论。但我认为,只要满足REST设计思想的功能描述方式,都可以算作REST的实现,其并不局限于HTTP协议。
理解REST
Representational State Transfer,其实已经将REST的整体概念罗列出来了,加上我们补充的主语“资源”,可以很明确的体现出REST中主要的两个概念:
- 资源,资源表征
- 动作,状态转移
简单理解的话,REST是就是将一个接口动作的描述进行拆分,拆分成资源与动作两个部分。其中,资源就是对描述资源位置,资源表征则是这些资源应该如何展示出来(具体是JSON还是XML),而状态转移则可以简单的理解成正对这个资源所进行的动作。
REST的正是通过将这两种核心定义的逻辑进行分离、标准化,从而让对于“接口”、“操作”的定义更加便于理解,和可阅读(更完成权威的介绍可以参考https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm)。
资源
REST中的核心概念之一是对资源的的描述,而这个资源是一个抽象概念,并不一定是一个静态的资源,也可以是完整资源的一部分。实际上,只要是可以被引用的部分,我们都可以称之为一个资源。
而在web中我们标识一个资源使用的是URI(Uniform Resource Identifier)。从定义上来讲,URI是唯一标识符,即可以说是资源的名称也可以说是资源的地址。从这个角度上来说如果无法用URI来表示一个内容,那么就不能说它是一个资源。
一些正面的例子:
https://blog.csdn.net/losorick/article/details/123311537
https://xie.infoq.cn/article/3ddce663b21acd89f41582aa3
一些反面的例子:
/export/create
资源定义中多个名词之间用"-"(最好不用用"_",会在特定情况下显示不完全)来进行分割,并用"/"来表示资源增肌的概念。也可以用","或者";"来作为多个资源的分割。当然这些都是建议建议,对于具体的实施只要项目中统一就行,比如github中就是使用"..."来作为多个资源的分割,例如“/git/git/compare/master…next”
资源表征
URI只定位了具体的资源,但对于客户端来说是这是一个统一的抽象。如果客户端要使用的话并不能直接使用,需要指明所需要使用的资源形式。当前主流的文本交互方式表征是JSON格式,当然对于要求响应格式严谨的团队来说仍然有使用XML格式的,而对于媒体资源来说也有PNG、MP4等形式。
原则上,资源定位符只负责资源的标识,但是并不关注具体怎么展示资源,而需要用何种形式展示资源则是由客户端根据自己的需要申请的。在HTTP中我们服务端通常使用“content-type”来对资源的表征来进行描述,而客户端则是用“Accept”请求头中来指明所需要的格式类型。
同时,资源的表征并不局限在资源的输出类型上,资源间的关联关系也是资源表征的一部分。举例而言,当我们查询一个详情信息,查询后的响应体本身是一个资源,用于描述这个详情信息。如果需求要我们描述这个详情信息后的下一个详情信息的访问链接,用于提示用户进一步浏览。这个信息“下一个链接”的信息,如果我们放到响应体中似乎并不是很合适,因为这对于一个GET请求来说,这个“下一个链接”信息并不是资源本身的一部分。并且随着系统中推荐算法的运行,这部分信息甚至变成不可缓存的内容(因为可能随时发生改变)。基于以上讨论,或许我们增加一个LINK响应头用来描述“下一个链接”的信息,就达到了目的并保证了响应体对于资源本身描述的正确性。
所以,资源表征并非是对数据库的CURD,它体现了资源作为一个超媒体中一部分的这个概念(资源与资源的连接关系)。同时我们可以使用HTTP中的各个部分来独立的描述各种概念,而非一股脑的都丢到响应体中。
状态转移
通过上文中的描述,我们可以将资源理解成了一种静态的内容。那么该如何理解我们对资源的操作呢?REST是用状态转移来表述这件事的。当我们新建一个资源时,就将资源从“无”状态转移为了存在状态;当我们更新一个资源的时,则是让其内部的状态发生了更改。
REST中的是使用HTTP方法来描述这些操作类型的,这样的好处是,我们可以为操作类型指定统一的规范。举个例子来说,在REST中所有的GET请求都应该是可以被缓存的,所有的PUT请求都应该是幂等的。我们通过将接口操作约束成明确的HTTP方法或者是其他统一方式,从而减少在接口对接查看时的沟通成本。
常用的HTTP方法主要是:GET、POST、PUT、DELETE。但是更早版本的客户端可能只有GET和POST。而根据协议的升级则支持LOCK、UNLOCK等方法以及自定义方法。但通常企业内会根据自己的主要受众设备进行调整这些设计。
从Restful出发的接口规范
对于一个接口,其中的URI部分应该只用于描述操作时针对哪个资源的。而“HTTP方法”应该才用于解释操作的类型的。但是企业中如果要推行REST接口的规范的话,仍然有一些问题需要调整确认,原因可能是内部历史原因或者当前框架与REST并不适配,本小节举例其中的两个例子用于各位参考。
向下兼容
由于在业务实际开发的过程中,可能会出现业务的逻辑变更,我们处理这种问题的主要方法就是通过对接口添加版本信息来实现的。
但是由上文可知,URI本身应该用于定义资源的名称和地址。所以对于同一个资源来说,内容变了就是变了,资源本身是没有版本的概念,我们实际上调整的是资源的不同的表征方式,而这个方式才对应的“版本”的概念,而非资源本身。如果是从这个角度出发,对于REST的设计来说,这个版本的概念就不应该出现在URI的资源定位符上,因为资源的名称都是同一个。那么对于REST我们可以通过在Accept响应头追加版本信息(version)来区分具体的表征方式。例如:
- Accept: version=1.0
- Accept: version=2.1
- Accept: version=3.0
但是在实际的企业中,我们通常不会这么做。原因有很多,其中一种是因为在业务沟通的时候,通常只聚焦在URI和HTTP方法上。在实际沟通中我们将URI和HTTP方法作为互相沟通的主要方法,并可以通过一行就表示出全部信息,例如:
{GET} http://api.example.com/trade/order/1
所以如果在URI地址上直接添加版本信息,就可以通过以下方式表示:
- {GET} http://api.example.com/trade/v1/order/1
- {GET} http://api.example.com/trade/v2/order/1
- {GET} http://api.example.com/trade/v3/order/1
这样的好处就是可以通过URI直接描述兼容性信息,而缺点就是破坏了REST原教旨主义的资源定位方法。但REST推出到当前已有20多年时间,而实际业务中“资源”数量也已经爆炸数量增长。所以在服务治理等各种新概念成为必要需求的今天,符合现状的调整才是合适的。
定制的操作
在REST中我们需要用HTTP的方法来定义操作的类型,那么就有一个主要问题:已有方法无法描述当前操作怎么办。一些常见的操作(Postman中有的)是PATCH(github有)、COPY、LINK等。除此之外常见的还有BATCH- CREATE等批量操作。
如果根据REST原教旨主义则应该在HTTP方法中进行扩展,但由于系统兼容性等问题,我们希望保证各种版本的客户端可以对方法进行支持。
首先,不论我们是使用什么方法来表示自定义操作,都需要满足统一表达,并且可以应用在所有的方法中。根据这种情况,本小结列举出几种方案作为参考:
扩展方法
HTTP中的方法可以自己扩展,Github新增了PATCH方法,WebDAV中扩展了LOCK、UPLOCK等方法,但这些都不是HTTP中的标准方法,如果使用该方法需要考虑客户端的支持能力。
参数定义
通过使用预留参数定义扩展方法,例如使用_method=DELETE来对请求方法来定义,并在服务端对具体方法进行路由。这种方法的好处是可以将参数代入到URI中,并不影响URI原版本的资源定位的意义。
URI后缀描述
可以通过在URI添加后缀来描述动作,例如在后缀添加/actions:delete来定义,或者在URI最后使用/actions标记,并在请求体中第一层来描述扩展的动作类型。这样虽然导致URI定义中加入了额外的资源地址之外的额外信息,但是可以保证整体访问接口中的信息是完整的。
总体来说,对于确定定制操作的扩展方案要在客户端支持成都、下游网关改造难度以及接口可读性中做出平衡。
Restful风格的问题
使用Restful风格的构建项目中主要需要的问题就是关于path-variable的处理问题。在一些项目治理项目中默认是认为有path-variable引起的不同path是不同的URL,所以无法直接使用,需要具有开源软件二次开发的能力和需求。
举一个Sentinel的例子,Sentinel是根据url生成的资源名称,而因为REST中用path-variable来定义资源,所以就导致了同一类资源的定义符被识别成了不同的资源。
但随着查看Sentinel中的代码我们可以发现,Sentinel中默认使用CommonFilter来处理请求的url,并且主要是通过UrlCleaner接口中的clean方法来对资源进行重命名,所以我们可以通过重写clean方法实现满足rest的资源明明问题(事实上官方也提供了sentinel-spring-webmvc-adapter来支持rest风格定义的接口)。
由于REST风格是主流的接口规范风格之一,所以使用量较大的中间件一会都会有对应的解决方案,但是对于自研的工具需要考虑REST风格的兼容性,可以参考一些开源软件如Swagger的匹配来实现。
最后
本文讨论了REST中的相关概念,并非完全照本宣科的进行陈述,如REST的6大指导原则也并没有介绍。REST本身是一个比较大的概念,但是在如今前已经后端分离、微服务化等概念逐渐普及,原本的REST概念并非完全的适用。但是REST的核心理念对我们的接口规范设计有十分重要的指导性意见。