写点什么

理解 Restful 风格

  • 2022 年 3 月 10 日
  • 本文字数:3950 字

    阅读完需:约 13 分钟

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 的核心理念对我们的接口规范设计有十分重要的指导性意见。

发布于: 刚刚阅读数: 2
用户头像

人肉bug制造机 2020.06.26 加入

欢迎关注同名公众号!

评论

发布
暂无评论
理解Restful风格_RESTful_蜜糖的代码注释_InfoQ写作平台