【问题标题】:REST API design: what is a unique operation or resourceREST API 设计:什么是独特的操作或资源
【发布时间】:2020-12-26 09:57:08
【问题描述】:

几年前,我创建了a tiny web service,它以两种表示形式提供相同的资源。

# returns a collection of Foos
GET /foo
# returns the same collection of Foos in a different JSON representation
GET /foo?projection=X with 'Accept: my-specific-media-type'

这在 (Java) 代码中非常有效,因为我可以将两个方法映射到相同的 @Path 并具有不同的返回类型。一个接受 @QueryParam@Consumes 特定的媒体类型,而另一个不接受。

但是,根据(当前)@ApiOperationSwagger 注释,我选择了错误的 API 设计。

HTTP 方法和路径的组合创建了唯一的操作

因此,在我将旧项目升级到当前库版本后,Swagger 模型仅包含一个 GET /foo 操作 - 这是随机的,因为它依赖于通过 Java 反射进行的运行时代码自省。

所以,问题是这样的:Foo 资源以不同的表示形式实际上是“相同”资源还是不同的资源? Swagger 注释似乎暗示了后者(不同的资源 -> 不同的路径)。

【问题讨论】:

  • 通常使用内容类型协商(即使用AcceptContent-Type 标头)让服务器和客户端确定使用哪种表示格式。在 HTML 表单的情况下,即发送到服务器的媒体类型要么是隐式 (application/x-www-form-urlencoded),要么是显式给出。不幸的是,Swagger has hardly anything to do with REST
  • 是的,我完全了解内容类型协商的注意事项。不管客户端如何表达它想要使用的表示,都存在Foo 的表示是否构成资源的问题。 Foo 是资源还是 Foo-as-JSON 是资源? Swagger 与 REST 有很大关系,因为 Swagger 成为 OpenAPI 2 规范,这反过来又导致了 OpenAPI 3。你链接到的 Q 谈论了 Swagger 与 HATEOAS。在 OpenAPI 2 和 3 中,您都不能指定具有多种不同响应类型的路径。
  • REST 架构中的资源是untyped。通常,数据,无论其表示形式如何,都定义了资源。表示只是该日期相对于所选媒体类型的具体实例。同样,Swagger 与 REST 没有太大关系。如果是这样,它就会对现在和实际上更接近 RPC 而不是 REST 的情况做出错误的描述。当然,对于那些散布错误含义的“伪 REST”API,您可能会觉得它确实属于 REST 堆栈,但它并不

标签: rest swagger jax-rs openapi


【解决方案1】:

您遇到的部分问题是 REST 概念和 Swagger/OpenAPI 概念的混合。

Resource 是一个 REST 概念:“任何可能成为作者超文本引用目标的概念都必须符合资源的定义”

Representation 是一个 REST 概念:“表示是一个字节序列,加上用于描述这些字节的表示元数据。”

Operations 是一个 OpenAPI 概念:“OpenAPI 将唯一操作定义为路径和 HTTP 方法的组合。”

这里存在一定程度的紧张,因为这些观点实际上并不一致。

例如,从 REST 的角度来看,没有理由记录“GET 操作”,因为 GET 是统一接口的一部分 - 无论使用什么值作为目标 uri,它都具有相同的语义。这是使万维网成为可能的关键架构约束的一部分——一致的语义意味着我们可以使用通用组件(如 web 浏览器)与 all网络上的不同资源。


不同表示形式的 Foo 资源实际上是“相同”资源还是不同的资源?

“视情况而定”。

“一个资源,不同表示”的典型示例是一张图片,其中我们可能有 GIF、JPEG、PNG、BMP。相同的图片(ish),但需要以不同方式处理的不同字节序列。

同样,您可能有一个网页 (HTML),还有一个 text/plain 表示或 JSON 表示等。

要问的一个重要问题:通用缓存是否具有返回请求“正确”表示所需的信息?

也就是说:鉴于您的原始设计使用查询参数来区分一个投影与另一个投影,您可能应该尊重这种本能并继续将不同的表示视为属于不同的资源(这意味着通用缓存将保留它们完全分开)。

这是否意味着您要共享相同的路径 /foo(将 projection 视为可选的@ApiParam),或者为每个投影提供不同的路径(为每个唯一路径定义单独的操作)是不太清楚。在棕地项目中,我倾向于记录你已经拥有的东西,而不是进行大量的重大更改。

但将“易于记录”视为设计约束当然是合理的。

【讨论】:

  • 感谢您提供平衡 REST 和 OpenAPI 的深思熟虑的回答!几年来我没有从事那个小项目,只是意识到我实际上是am 通过Accept 标头使用内容协商。因此,我还在Vary 标头中添加了Accept,以便更好地使用缓存。 projection 参数仍然很重要,但不是必需的。我相应地更新了 Q。但是,图片并没有太大变化。在过去几年的某个时候,Sw​​agger/OpenAPI 社区决定将我的两个操作视为一个而不是两个。
【解决方案2】:

那么,问题是这样的:不同表示形式的 Foo 资源实际上是“相同”资源还是不同的资源?

Fielding 将资源定义为:

REST 中信息的关键抽象是资源。 任何可以命名的信息都可以是资源:文档或图像、时间服务(例如“洛杉矶今天的天气”)、其他资源的集合、非虚拟对象(例如一个人),等等。换句话说,任何可能成为作者超文本参考目标的概念都必须符合资源的定义。资源是到一组实体的概念映射,而不是在任何特定时间点对应于映射的实体。

更准确地说,资源 R 是一个随时间变化的隶属函数 MR(t),它在时间 t 映射到一组等效的实体或值。集合中的值可以是资源表示和/或资源标识符。一个资源可以映射到一个空集,这允许在一个概念的任何实现存在之前对该概念进行引用——这个概念对于 Web [61] 之前的大多数超文本系统来说是陌生的。 某些资源是静态的,因为在创建后的任何时间检查它们时,它们总是对应于相同的值集。 随着时间的推移,其他人的价值差异很大对于资源来说,唯一需要静态的就是映射的语义,因为语义是一种资源与另一种资源的区别。

...

REST 使用资源标识符来标识组件之间交互所涉及的特定资源。 REST 连接器提供了一个通用接口,用于访问和操作资源的值集,而不管成员函数是如何定义的或处理请求的软件类型如何。分配资源标识符的命名机构,使得引用资源成为可能,负责随着时间的推移维护映射的语义有效性(即,确保成员函数不会改变)。 (Source)

简而言之,资源是您为以后引用而命名的东西。该资源是数据的容器。该数据可以通过多种方式表示。表示是资源数据的具体实例,与创建表示的媒体类型有关。媒体类型本身定义了具体实例的语法和语义。 IE。 HTML 定义了负载中允许的属性和元素以及这些内容表达的内容。

应使用 REST shouldn't have typed "resources" meaningful to clients 内容类型协商。在这里,客户端通过Accept 标头向服务器表达其功能,服务器将选择最适合数据的表示格式。表现良好的服务器只会在建议的媒体类型中进行选择,因为它知道客户端可以处理数据。行为不端的客户端只会忽略标头并发送它想要的任何内容,这最终可能会阻止客户端完全处理有效负载。

REST 就是将客户端与服务器解耦,并允许服务器端在不破坏客户端的情况下在未来发展。然而,这只有在两者都使用某种间接方式时才有可能。 IE。不是 URI 本身是有效负载中的相关事物,而是附加到该 URI 的链接关系。对于可遍历的集合,链接关系可能类似于nextprevfirstlast,或者类似于prefetch,女巫只是声明一旦客户端加载了带注释的URI 的内容,就可以加载所有其他内容,目前处于空闲状态,因为接下来可能会请求此内容。这种链接关系要么是standardized,要么应该遵循Web Linking中定义的扩展机制。

关于您的实际问题。想想任意产品ABC1234。该产品包含一些属性,例如其价格、当前库存商品数量、一些描述该产品的元数据等等。这些属性可以用 JSON、XML 或 HTML 表示。能够处理这些媒体类型的客户端将能够创建具有相同属性的“对象”而几乎没有任何问题。实际使用的表示格式不应影响资源本身的实际数据。毕竟,表示格式只是客户端和服务器之间交换数据的一种双方同意的方式,以便允许有效负载的接收者以发送者最初想要的相同方式处理它。

正如菲尔丁之前提到的,这样的资源可能是静态的,也可能会随着时间而变化。对于上面的产品示例,价格可能会随着时间而变化,但这不会改变实际产品的语义。随着时间的推移,有时需要将存在于资源中的相同数据作为其他资源的一部分提供。这完全没问题,这里的事情开始变得更有趣了。作为公司合并的一部分,我们的一位客户需要以不同的名称公开他们所有的项目。在他们的案例中,他们选择同时提供两个产品名称一年。根据定义,对于任意 HTTP 客户端来说,这将是两种不同的资源,即 ABC1234XYZ12345,即使它们“代表”同一实时产品的数据。他们还可以选择使用(永久)将客户端重定向到“新”URI,从而提示客户产品实际上是相同的。

如果您看一下缓存在 HTTP 生态系统中的工作原理,那么每个名称(或 URI)的资源概念也很明显。这里effective request URI 用作缓存键,以查找请求的URI 是否已经存在存储的响应。对该 URI 执行的任何不安全操作都将导致该存储的响应被驱逐。这就是为什么 HTTP 不适用于批处理操作的原因之一,因为它们可能完全绕过缓存并导致错误和/或误导性结果。

几年前,我创建了一个小型 Web 服务,它以两种表示形式提供相同的资源。

GET /foo               # returns a collection of Foos
GET /foo?projection=X  # returns a collection of Foos in a different coordinate system i.e.  different representation

根据 HTTP 定义有效请求 URI 的方式,这两个 URI 实际上将针对两个不同的资源,尽管它们表达相同的数据只是具有不同的表示。一个可能更好的方法是只公开/foo 并使用不同坐标系的专用媒体类型,或者甚至更好的支持profiles 的媒体类型,并通过配置文件属性提示收件人处理器“那种" 它接收到的数据。如上所述,链接关系还定义了一个 profile 关系名称,可用于允许客户端在返回“公制”或“英制”、“开尔文”、“华氏”或“摄氏度”或类似的 URI 之间进行选择测量数字等。

因此,长话短说,松散地谈论绝对 URI,包括矩阵、查询和路径参数,是在任意客户端“命名”资源的内容。毕竟,整个 URI 是该资源的标识符。稍微不同的名称可能会导致本地或中间缓存未命中,因此表示不同的资源,即使表示的数据与以前相同。可以使用同一资源上的内容类型协商或配置文件来“摆脱”仅在返回的表示格式不同方面不同的同级“资源”,而不是使用两个略有不同的 URI 重定向指令。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-09
    • 1970-01-01
    • 2021-09-02
    • 2020-10-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多