【问题标题】:Versioning REST API版本控制 REST API
【发布时间】:2012-05-31 08:07:17
【问题描述】:

在阅读了大量有关 REST 版本控制的材料后,我正在考虑对调用而不是 API 进行版本控制。例如:

http://api.mydomain.com/callfoo/v2.0/param1/param2/param3
http://api.mydomain.com/verifyfoo/v1.0/param1/param2

而不是先拥有

http://api.mydomain.com/v1.0/callfoo/param1/param2
http://api.mydomain.com/v1.0/verifyfoo/param1/param2

然后去

http://api.mydomain.com/v2.0/callfoo/param1/param2/param3
http://api.mydomain.com/v2.0/verifyfoo/param1/param2

我看到的优势是:

  • 当调用发生变化时,我不必重写整个客户端 - 只需重写受更改调用影响的部分。
  • 客户端中那些可以继续工作的部分可以继续工作(我们投入了大量的测试时间来确保客户端和服务器端都稳定。)
  • 我可以对已更改的呼叫使用永久或非永久重定向。
  • 向后兼容将是轻而易举的事,因为我可以保留旧的呼叫版本。

我错过了什么吗?请指教。

【问题讨论】:

标签: rest


【解决方案1】:

需要一个 HTTP 标头。

Version: 1

Version 标头临时注册在RFC 4229 中,有一些合理的理由可以避免使用 X- 前缀或特定用途的 URI。 yfeldblumhttps://stackoverflow.com/a/2028664 提出了一个更典型的标头:

X-API-Version: 1

在任何一种情况下,如果标头丢失或与服务器可以交付的内容不匹配,请发送 412 Precondition Failed 响应代码以及失败的原因。这要求客户端每次都指定它们支持的版本,但会在客户端和服务器之间强制执行一致的响应。 (可选地支持?version= 查询参数将为客户端提供额外的灵活性。)

这种方法简单、易于实施且符合标准。

替代方案

我知道一些非常聪明、善意的人建议使用 URL 版本控制和内容协商。两者在某些情况下和通常提出的形式都有重大问题。

网址版本控制

如果您控制所有服务器和客户端,则端点/服务 URL 版本控制有效。否则,您将需要处理回退到旧服务器的较新客户端,最终您将使用自定义 HTTP 标头进行处理,因为部署在您无法控制的异构服务器上的服务器软件的系统管理员可以做各种各样的事情来搞砸如果您使用 302 Moved Temporarily 之类的内容,您认为很容易解析的 URL。

内容协商

如果您非常关心遵循 HTTP 标准但又想忽略 HTTP/1.1 标准文档的实际内容,则可以通过 Accept 标头进行内容协商。您倾向于看到的建议的 MIME 类型是 application/vnd.example.v1+json 的形式。有几个问题:

  1. 当然,在某些情况下,供应商扩展实际上是合适的,但客户端和服务器之间的通信行为略有不同,并不真正符合新“媒体类型”的定义。此外,RFC 2616 (HTTP/1.1) 写道,“媒体类型值已在 Internet 号码分配机构注册。媒体类型注册过程在 RFC 1590 中进行了概述。不鼓励使用未注册的媒体类型。”我不想为每个具有 REST API 的软件产品的每个版本都看到单独的媒体类型。
  2. 任何子类型范围(例如,application/*)都没有意义。对于将结构化数据返回给客户端进行处理和格式化的 REST API,接受*/* 有什么好处?
  3. Accept 标头需要一些努力才能正确解析。应该遵循隐含和明确的优先级,以最大程度地减少实际正确进行内容协商所需的来回。如果您担心正确实施此标准,那么正确执行这一点很重要。
  4. RFC 2616 (HTTP/1.1) 描述任何不包含 Accept 标头的客户端的行为:“如果不存在 Accept 标头字段,则假定客户端接受所有媒体类型。” 所以,对于您不自己编写的客户端(您控制最少的地方),最正确的做法是使用服务器知道的最新、最容易破坏的旧版本响应请求。换句话说,您可能根本没有实施版本控制,而这些客户端仍然会以完全相同的方式中断。

2014 年编辑

我已经阅读了很多其他答案和每个人的深思熟虑的cmets;我希望通过几年的反馈可以改进这一点:

  1. Don't use an 'X-' prefix。我认为Accept-Version 可能在 2014 年更有意义,并且对于在 cmets 中提出的重复使用 Version 的语义存在一些有效的担忧。与 Content-Version 之类的已定义标头和 URI 的相对不透明性肯定存在重叠,我尽量小心地将两者与内容协商的变体混淆,Version 标头实际上就是这样。 URL https://example.com/api/212315c2-668d-11e4-80c7-20c9d048772b 的第三个“版本”与“第二个”完全不同,无论它包含数据还是文档。
  2. 关于我上面所说的关于 URL 版本控制(例如https://example.com/v1/users 之类的端点),反过来可能更符合事实:如果您控制所有服务器和客户端,那么 URL/URI 版本控制可能就是您想要的。对于可以发布单个服务 URL 的大型服务,我会为每个版本使用不同的端点,like most do。我的特别看法深受以下事实的影响:上述实现通常由许多不同的组织部署在许多不同的服务器上,也许最重要的是,部署在我无法控制的服务器上。我总是想要一个规范的服务 URL,如果一个站点仍在运行 API 的 v3 版本,我绝对不希望 https://example.com/v4/ 的请求返回他们的 Web 服务器的 404 Not Found 页面(或者更糟的是,200 OK 将他们的主页作为 500k 的 HTML 通过蜂窝数据返回给 iPhone 应用程序。)
  3. 如果您想要非常简单的 /client/ 实现(以及更广泛的采用),那么对于客户端作者来说,在 HTTP 请求中要求自定义标头就像 GET-ting 普通 URL 一样简单,这是非常困难的。 (尽管身份验证通常需要在标头中传递您的令牌或凭据。使用VersionAccept-Version 作为秘密握手以及实际 秘密握手非常适合。)李>
  4. 使用Accept 标头的内容协商非常适合为相同的内容获取不同的 MIME 类型(例如,XML 与 JSON 与 Adob​​e PDF),但未针对这些内容的版本进行定义(都柏林核心 1.1 与 JSONP与 PDF/A)。如果您想支持 Accept 标头,因为尊重行业标准很重要,那么您不希望虚构的 MIME 类型干扰您可能需要在请求中使用的媒体类型协商。保证定制的 API 版本标头不会干扰大量使用、经常被引用的 Accept,而将它们混为一谈只会让服务器和客户端感到困惑。也就是说,出于多种原因,将您期望的内容命名为每个 2013 年的 RFC6906 的命名配置文件比单独的标题更可取。这非常聪明,我认为人们应该认真考虑这种方法
  5. 为每个请求添加标头是在无状态协议中工作的一个特殊缺点。
  6. 恶意代理服务器几乎可以做任何事情来破坏 HTTP 请求和响应。 They shouldn't,虽然我不在这里讨论 Cache-ControlVary 标头,但所有服务创建者都应该仔细考虑他们的内容如何在许多不同的环境中使用。

【讨论】:

  • 写得好!我希望在最后对您的四个要点进行更多说明。你说“这对正确很重要”,但在这种情况下什么是正确的?
  • 如果您正在解析 Accept 标头,您需要正确处理 1) 排序、2) 通配符和 3) '质量因子',它会在计算哪种响应时更改加权值返回。更多w3.org/Protocols/rfc2616/rfc2616-sec14.html
  • 听起来完全是您不应该自己做的事情,但请使用内置该算法的库。
  • @JoeLiversedge 感谢您的出色回答,我非常喜欢阅读您对这些不同方法的看法。我想知道您是否可以扩展您的答案以分享您对 Matthew 在另一个答案中分享的 profile-media-type 方法的看法。
  • 据我从您从 w3 提供的链接中了解到,版本标头旨在用于版本控制对象而不是资源,它说:...当使用 PUT 重新提交已编辑的对象时例如,此字段给出 Version 的值。这通常允许服务器检查例如不同方的两个并发修改不会丢失......它似乎是一个标头,用于识别不在结构中的数据更改,api版本控制背后的想法是向后资源结构的兼容性与数据历史无关。
【解决方案2】:

这是一个见仁见智的问题;这是我的,以及意见背后的动机。

  1. 在 URL 中包含版本。
    对于那些说它属于 HTTP 标头的人,我说:也许。 但是根据该领域的早期领导者,放入 URL 是公认的做法。 (谷歌、雅虎、推特等)。这是开发人员所期望的,做开发人员所期望的,换句话说,按照最小惊讶的原则行事,可能是一个好主意。它绝对不会让“客户更难升级”。如果 URL 的更改以某种方式对消费应用程序的开发人员构成了障碍,正如此处不同答案中所建议的那样,则需要解雇该开发人员。

  2. 跳过次要版本
    有很多整数。你不会用完的。你不需要小数点。您的 API 从 1.0 到 1.1 的任何更改都不应该破坏现有的客户端。所以只使用自然数。如果你喜欢用分离来暗示更大的变化,你可以从 v100 开始,然后做 v200 等等,但即使在那里,我认为YAGNI 也是矫枉过正。

  3. 将版本放在 URI 的最左边
    据推测,您的模型中将有多个资源。它们都需要同步进行版本控制。你不能让人们使用资源 X 的 v1 和资源 Y 的 v2。它会破坏一些东西。如果您尝试支持它,它将在您添加版本时创建维护噩梦,并且无论如何对开发人员都没有任何附加值。所以,http://api.mydomain.com/v1/Resource/12345,其中Resource 是资源的类型,12345 被资源ID 替换。

你没有问,但是……

  1. 在 URL 路径中省略动词
    REST 是面向资源的。你的 URL 路径中有诸如“CallFoo”之类的东西,它看起来很像动词,不像名词。这是错误的。 Use the Force, Luke. 使用属于 REST 的动词:GET PUT POST DELETE 等等。如果您想对资源进行验证,请执行GET http://domain/v1/Foo/12345/verification。如果你想更新它,请POST /v1/Foo/12345

  2. 将可选参数作为查询参数或有效负载
    可选参数不应位于 URL 路径中(在第一个问号之前),除非您建议这些可选参数构成独立资源。所以,POST /v1/Foo/12345?action=partialUpdate&param1=123&param2=abc

【讨论】:

  • 对不起,我才看到这个...优点“省略动词...”和“放置可选参数...”。
  • 在您的第 3 点中。我认为将版本放在 uri 的最左侧会更有问题,在 REST 中,您使用映射到实体的资源。在大多数情况下,多个资源是分开发展的,例如,用户拥有一个与客户资源无关的新字段。在实现方面,如果您在 url 的根级别执行此操作,那么即使它们没有更改,您也需要向所有资源添加新的映射。我认为您的担忧更多是在使用错误版本实体的算法层面,但这与 REST API 版本无关。
  • @raspacorp 将版本放在正确的位置是您在实践中使用过的吗?通常,从我查看的 API 来看,版本位于资源\实体标识符的左侧。我现在有一个困惑,因为这就是我为一组无服务器服务启动我的 API 的方式,例如v1/resource/sub_resource。我现在已经开始研究第二个 API,只是发现 API 网关不允许我部署一个以 /v1 作为基本路径的单独无服务器项目。向右移动会缓解这个问题,但如果没有操作或子资源,它看起来很奇怪。例如resource/v1
  • @raspacorp 你知道任何使用这种版本控制方法的公共 API,我可以看看吗?
  • @berimbolo 我只在我工作过的公司的私人和商业 API 中看到它,但只有两个深度级别,例如:...accounts/v1/orders/v1/items,我同意没有子资源看起来很奇怪
【解决方案3】:

不要做这两件事,因为它们会将版本推送到 URI 结构中,这将对您的客户端应用程序产生不利影响。这将使他们更难升级以利用您应用程序中的新功能。

相反,您应该版本化您的媒体类型,而不是您的 URI。这将为您提供最大的灵活性和进化能力。欲了解更多信息,请参阅this answer我给了另一个问题。

【讨论】:

  • 根据stackoverflow.com/questions/389169/…,一些代理和中间服务器会去除标头(可能包括媒体类型)。这可能是一个大问题。还是我理解不正确?
  • 我无法想象代理或中间服务器会剥离标准的 Accept 标头。
  • 如果 URL 的更改以某种方式对消费应用程序的开发人员构成了障碍,正如答案中所建议的那样,该开发人员需要被解雇。 URL 中的v1 并不是开发人员升级和“利用应用程序中的新功能”的真正障碍。
  • 版本控制应该与适当的和现有的内容协商技术隔离,而不是与 RESTful 应用程序的最基本结构(URI)隔离。否则会在升级时把婴儿和洗澡水一起扔出去。
  • 版本控制可能不仅与内容有关,还与 API 结构和语义有关。
【解决方案4】:

我喜欢使用配置文件媒体类型参数:

application/json; profile="http://www.myapp.com/schema/entity/v1"

更多信息:

https://www.rfc-editor.org/rfc/rfc6906

http://buzzword.org.uk/2009/draft-inkster-profile-parameter-00.html

【讨论】:

  • RFC 6906 和本 IETF 草案如何相互关联? RFC 6906 似乎是明确的标准,但它没有说明如何在 AcceptLink 标头中使用 profile
【解决方案5】:

这取决于您在 API 中调用的版本,如果您将版本调用到实体的不同表示(xml、json 等),那么您应该使用接受标头或自定义标头。这就是 http 为处理表示而设计的方式。它是 RESTful 的,因为如果我同时调用同一个资源但请求不同的表示形式,返回的实体将具有完全相同的信息和属性结构但格式不同,这种版本控制是装饰性的。

另一方面,如果您将“版本”理解为实体结构的变化,例如将字段“年龄”添加到“用户”实体。然后你应该从资源的角度来处理这个问题,我认为这是 RESTful 方法。正如 Roy Fielding 在他的论文中所描述的那样...... REST 资源是从标识符到一组实体的映射......因此,在更改实体的结构时,您需要拥有指向该实体的适当资源是有道理的版本。这种版本控制是结构化的。

我在http://codebetter.com/howarddierking/2012/11/09/versioning-restful-services/中发表了类似的评论

在使用 url 版本控制时,版本应该在 url 中较晚而不是较早:

GET/DELETE/PUT onlinemall.com/grocery-store/customer/v1/{id}
POST onlinemall.com/grocery-store/customer/v1

另一种更简洁的方式,但在实施时可能会出现问题:

GET/DELETE/PUT onlinemall.com/grocery-store/customer.v1/{id}
POST onlinemall.com/grocery-store/customer.v1

这样做允许客户端专门请求他们想要映射到他们需要的实体的资源。不必弄乱在生产环境中实施时确实存在问题的标头和自定义媒体类型。

在 url 后面加上 url 可以让客户端在选择他们想要的资源时有更多的粒度,即使是在方法级别。

但从开发人员的角度来看,最重要的是,您不需要维护每个版本到所有资源和方法的整个映射(路径)。当您拥有大量子资源(嵌入式资源)时,这非常有价值。

从实现的角度来看,将其置于资源级别确实很容易实现,例如如果使用 Jersey/JAX-RS:

@Path("/customer")
public class CustomerResource {
    ...
    @GET
    @Path("/v{version}/{id}")
    public IDto getCustomer(@PathParam("version") String version, @PathParam("id") String id) {
         return locateVersion(version, customerService.findCustomer(id));
    }
    ...
    @POST
    @Path("/v1")
    @Consumes(MediaType.APPLICATION_JSON)
    public IDto insertCustomerV1(CustomerV1Dto customer) {
         return customerService.createCustomer(customer);
    }

    @POST
    @Path("/v2")
    @Consumes(MediaType.APPLICATION_JSON)
    public IDto insertCustomerV2(CustomerV2Dto customer) {
         return customerService.createCustomer(customer);
    }
...
}

IDto 只是一个返回多态对象的接口,CustomerV1 和 CustomerV2 实现了该接口。

【讨论】:

    【解决方案6】:

    Facebook 会验证 in the url。我觉得 url 版本控制在现实世界中也更干净、更易于维护。

    .Net 使以这种方式进行版本控制变得超级容易:

    [HttpPost]
    [Route("{version}/someCall/{id}")]
    public HttpResponseMessage someCall(string version, int id))
    

    【讨论】:

      猜你喜欢
      • 2018-09-30
      • 2014-08-29
      • 2012-12-25
      • 2017-03-18
      • 2015-03-10
      • 1970-01-01
      • 2021-06-22
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多