前言
REST (Representational State Transfer) 自从2000年由Roy Fielding博士提出来,得到了广泛的接受和支持,已经成为了事实上API开发的标准方案。其 HATEOAS(Hypermedia as the Engine of Application State,超媒体作为应用状态引擎)则是对于资源的导航和发现做出了详尽的定义。
时过26年后,这套规范依然应用广泛。但是在一些细节上,已经难免落后于现代的需求了。 比如其核心约束 HATEOAS, 作为资源的自发现和导航,实际上用处非常的有限。 一些严谨的系统中,会遵循HATEOAS在返回中增加相关的导航链接。但是大部分系统基本都忽略了这个机制。即便是尊重返回导航链接的API实现中,返回的链接也非常有局限性,缺乏通用性。
本规范试图在这个基础上,对资源进行概念性明确的细分和定义,从而描述一套资源的完整自发现的规则, 使得前端可以通过API,就可以获取完整且详尽的资源列表和资源定义。由此前端完全可以采取一个通用的不变的固定框架,来处理任意的REST 资源定义。 符合本规范的情况下,对于后端来说,可以随时调整API的内容;对于前端来说,则可以不做任何变更,自动适应后台的最新的API.
需要注意的是,超媒体契约(RHC)不适合所有的场景。超媒体契约的理想目标是后端API可以任意的,快速的迭代,变更,于此同时前端无需做任何修改即可体现后端的api的效果。 解决的是前后端耦合过紧导致开发流程冗长,缓慢的问题。 在这个场景下,API的调用方,称为前端。也就是一个最终的端点。不会有后续的业务依赖。
对于非超媒体契约所适用的场景,举例来说,后端A提供的API的调用方是后端B。 那么后端B的相关业务,甚至他提供的API,都可能依赖于后端A的API. 这种情况下,完全自动的API自发现,自适应,可能会有风险蔓延,业务断层等的危险。 这种情况下就比较适用文档化,固化不变的API定义。
超媒体契约比较经典的应用场景是管理后台。 尤其是硬件类,后台服务类的运维管理。 这种场景下,前端是比较确定的网页应用,不存在下一步的业务依赖。
另一个比较典型的应用是为AI提供服务。由于API接口自带说明,从而为AI调用带来极大的便利性。
核心思想
REST 将一切用途,统一为资源。所有的操作,都是针对资源的标准化操作。 这种高层级的抽象使得所有不同类型的应用,其解决思路都非常的清晰和一致。
超媒体契约(RHC)则是在这一层上作进一步的拆分和定义。
RHC将REST中单一的资源概念,细分为 空间和实体。 空间是容器,实体是原有的资源概念。 如果类比的话,可以理解为空间是目录, 实体是文件。而原有的REST只定义了全路径的文件这一个概念。
具体来说, 在REST中,一个API的端点可能是 /api/v1/erm/account 。 这个全路径作为一个完整且单一的端点。 但是从 超媒体契约(RHC)看来,这个全路径里, /api, /api/v1, /api/v1/erm 都是空间,都有含义。
在超媒体契约RHC中, 实体依然是原有的概念。(这意味着完整的兼容REST)。 但是对于空间, 超媒体契约 将他定义为包含子空间和实体的容器。 且支持一个统一的枚举操作。 换而言之, 你可以直接访问 空间API /api/v1/erm/, 而他也会统一的返回该空间下的所有子空间和实体。比如 user, account, order, flow, 以及 finance (namespace), salary (namespace), 等等。
额外补充一点的是,超媒体契约需要一个根的概念。可以理解为根目录。从这个根开始,可以通过读取空间来获得所有下一级的空间和资源。 就像从根目录开始,你可以用 ls, cd 来发现并查看任意的文件一样。
因此超媒体契约中,需要明确的定义根。到底是 /api, 还是/api/v1, 还是 /api/v1/erm。 需要明确的指定才能开始发现所有的实体资源。 而对于REST来说,只需要一个完整的路径 /api/v1/erm/account, 至于从哪儿开始的完全不关系。
概念定义
根或根空间
超媒体契约的起点位置。前端从这儿开始发现所有的资源。 根必然是空间,因此名称就叫根空间。 理解上可以视为根目录。
空间或空间资源
空间是实体的容器。 空间本身只具备一个统一的操作,就是枚举其包含的内容。内容可能是其他的空间,也可能是实体。 理解上可以视为文件夹,目录。
空间的定义使得REST可以更加的层次化。 通过空间将API分组,概念上更加清晰一些。
调用url为空间的API, 可以有一套通用且标准化的行为,如返回空间的详情,包括包含哪些子API等等。 这个是不受业务约束的顶层通用维度的。 如果是业务相关的,则可以自定义分组的规则在空间API上。比如权限控制类,分组策略等。
实体或者实体资源
等同于REST的资源的概念。
资源类型和执行操作
RFC2616, 7231, 5789, 9110中定义的HTTP方法共八种:GET,POST,PUT/PATCH,DELETE,HEAD,OPTIONS,TRACE,CONNECT.
REST没有明确的定义方法,但是基于他将万物视为资源的思想,绝大部分的研发设计人员都会将它往CRUD上靠,并对齐到POST,GET,PUT/PATCH,DELETE几个方法上。 这种对齐的方法,不仅简单可靠,事实上也能解决99%以上的问题。
但是仍存在一些很拗口,很生硬的资源化例子。比如登录/注销,比如转账, 比如上车,下车。 这些操作很难用合理、可理解、容易有基本共识的资源去做映射。 当前绝大部分系统中最这类的处理,处于尊重REST的角度,都是生造了一个资源去映射。因为很难理解,反而从而从事实上违背了REST要达成简单的思想共识的目标。
超媒体契约提出一个新的规范建议,即新增一种基本的方法类型为 EXECUTABLE。 也就是某些业务功能,他就是操作,作为资源去抽象反而更难理解。那么久单独的定义一个类型:执行。
登录/注销,转账等等,就是本身的 login, logout,transfer。 语义明确清晰。只是其类型明确的定义为 executable。 不能用CRUD的方法去操作他。唯一的操作就是直接调用执行。
这个思路显然是简单清晰的,但是绝大部分人在一开始这么想时,总是犹豫或者存疑,这是否RESTful?
为此我们可以从概念上澄清定义一下: 如果万物都是资源,资源就可以是文件。熟悉操作系统底层机制的朋友都知道,万物皆文件(句柄),是现代操作系统的无上利器。 既然万物都可以是文件,资源又有什么可以例外的呢?
而文件呢,有一个基本的属性,就是是否可执行。 可执行的,就是一个命令;不可执行的,才是什么图片啊,文本啊,words啊,ppt啊之类的。也就是在所有的文件类型之前,先分一个大类:是否可执行。 windows 下是用扩展名来区分的, linux中更纯粹一下。直接单独给另一个标记位x。 其实在操作系统中,所有的文件读取到系统,首先也必须区分的就是是否是可执行文件。可执行文件单独分区标记,并做严格的安全检查。
我们只需要将REST的资源,按照文件的概念,从类型上首先加一个是否可执行的标记,就可以完整的将动作/操作直接映射为资源,而不必费心拗口的单独去做资源化的抽象。
由此,我们只需要将REST的资源(RHC的实体资源),增加一个类型的定义,并且大类就是是否可执行,就可以完美的解决类似 登录/注销这类的RESTful语义混淆的问题了。
也可以认为CRUD用作业务处理是不够的,需要加一个X,CURDX才能完整的表征业务。
可执行资源类型的定义,将完善REST的细节,并且使得API的定义更加的合理化。
规则说明
完整的API自发现机制
对于前端来说,唯一需要知道的就是后端API的根URL。 由于根URL是空间资源,调用它就能获得它的下一级的空间和实体资源。如此遍历下去,就可以发现全部的API资源定义。
注1: 空间的发现和枚举机制,可以用通用的库来实现。与业务无关。这样子后台API只需要按照固定的容器形式来创建或是配置API,即可自动支持枚举和发现。
注2: 前端可以用同样的通用库实现,获得根URL后,自动展开他的API细节,并适配参数。
CRUD之外的X
对于HTTP API来说, CRUD的操作平面,目前已经有了比较通用且大家普遍接受的映射,及 POST, GET, PUT, DELETE。 这些映射,不仅是被浏览器、网关等普遍支持,同时也符合操作平面的幂等性和安全性 [^safetysecurity]要求。
对于X来说,他的幂等性和安全性都是最难的。常规通用的http method中,没有特别适合的映射,但如果只考虑幂等性和安全性的话,那就只有唯一的一个POST可以对应。 当然我们也可以扩展定义一个单独的方法来表示X的操作平面。 下面分别展开下。
方法1, 单独定义一个 EXECUTE 的 http method
实现起来相当的简单明了,在 POST,GET,PUT,DELETE之外,新增一个 EXECUTE。
存在的问题就是,新增的自定义方法,不再相关规范内,要将他注册成一个标准,需要比较长的时间去完成标准的讨论和定义。短时间内无法成为标准。
非标准的EXECUTE方法,可能存在的隐患主要是在复杂的网络环境中,适配性低。 虽然绝大部分的路由,网关,服务器都不会拦截, 但是一些代理,应用层网关,防火墙等等,有可能会拦截非标准的方法。
在简单的网络环境中,扩展EXECUTE是非常简单,且实用的方法。 这对于大部分的应用来说是足够的。 只是如果环境比较复杂,就要考虑下扩展EXECUTE的兼容性和可用性了。
方法2, 借用已有的POST,外加Content_Type约束
首先,适用于EXECUTE的标准的,常用的http方法中,只有POST在幂等性和安全性上最符合。 但是POST常规上已经被映射到CRUD的C。 所以这里要细分一下,同时细分机制也要早HTTP标准规范中借用,这样子兼容性才会最佳,可以无障碍运行于任何环境。
当然,向响应者增加自定义的http header, 也是最简单直接的方法,但是这里需要自定义,同样会碰到方法1里的兼容性问题,所以还不如用方法一。
在已有的http header中,Content Type 无论是从含义上,还是用法上,都非常接近于资源类型的设定。
因此我们可以从标准的http header中的Content Type里面,选取规范化定义好的MIME类型,来表示可执行的API资源类型。
ContentType, 一些称号也叫media type, 标准规范里面定义的类型非常的多。 完整的列表见这里:https://www.iana.org/assignments/media-types/media-types.xhtml
其中符合可执行的标准类型有两个,分别是 application/prs.implied-executable (https://www.iana.org/assignments/media-types/application/prs.implied-executable) 和 application/vnd.microsoft.portable-executable (https://www.iana.org/assignments/media-types/application/vnd.microsoft.portable-executable)
另外,需要注意的是,ContentType的内容是比较灵活的,此处即便是填入自定义的类型,绝大部分的系统都是兼容的。 因此,处于严格标准化的角度,可以选用prs.implied-executable 或 vnd.microsoft.portable-executable。 一般情况下,自行定义一个 executable 也是足够用的。
参考引用
- https://mimetype.io/all-types
https://www.iana.org/assignments/media-types/media-types.xhtml
https://www.iana.org/assignments/media-types/application/vnd.microsoft.portable-executable
- https://www.iana.org/assignments/media-types/application/prs.implied-executable
::: {#author name=reddish}
本文同步发表在 软件需求探索的https://srs.pub/thinking/restful-hypermedia-contract.html
作者: reddish@srs.pub
:::
[^safetysecurity]: Safety安全性和Security安全性. https://srs.pub/thinking/safety-security.html