如何对REST URI进行版本控制

114

如何最好地对 REST URI 进行版本控制?目前我们在 URI 本身中使用版本号,例如:

http://example.com/users/v4/1234/

适用于此表示形式的版本4。

版本号是否应该包含在查询字符串中?也就是说:

http://example.com/users/1234?version=4

或者说,版本控制最好通过其他方式来完成吗?


1
可能是 API版本控制的最佳实践? 的重复问题。 - Helen
11个回答

193

不要对URL进行版本控制,因为...

  • 你会打破永久链接
  • URL更改将像疾病一样传播到你的接口中。对于未更改但指向已更改表示形式的表示形式,你该怎么办?如果你更改了URL,就会破坏旧的客户端。如果你保留URL,你的新客户端可能无法工作。
  • 版本控制媒体类型是一个更灵活的解决方案。

假设你的资源返回的是应用程序/vnd.yourcompany.user+xml的某个变体,那么你所需要做的就是创建一个新的application/vnd.yourcompany.userV2+xml媒体类型的支持,并通过内容协商的神奇机制使v1和v2客户端可以和平共存。

在RESTful接口中,你拥有的与契约最接近的东西是在客户端和服务器之间交换的媒体类型的定义。

客户端用于与服务器交互的URL应该由服务器在先前检索到的表示形式中嵌入提供。客户端唯一需要知道的URL是接口的根URL。将版本号添加到URL中只有在客户端构造URL时才有价值,而这在RESTful接口中是不应该做的。

如果你需要对可能破坏现有客户端的媒体类型进行更改,那么就创建一个新的并保留你的URL!

对于那些当前正在使用application/xml和application/json作为媒体类型的读者来说,他们可能会认为这没有任何意义。我们应该如何对它们进行版本控制?不需要做版本控制。除非你使用代码下载来解析它们,否则这些媒体类型对于RESTful接口来说几乎无用,此时版本控制是无意义的。


70
解决这些要点:
  1. 不要更改永久链接,因为永久链接指向特定版本。
  2. 如果所有内容都有版本控制,那么这就不是问题。旧的 URL 仍然可以使用。理想情况下,您不希望版本 4 的 URL 返回与版本 3 资源的关联。
  3. 或许。
- Mike Pone
10
想象一下,如果您升级到新版本的网络浏览器后,所有您已添加为书签的网站都无法打开了!请记住,从概念上讲,用户保存的是资源的链接,而不是资源的某个版本或表现形式的链接。 - Darrel Miller
11
为了满足 REST API 自我描述的要求,需要在 content-type 头部提供完整的语义描述信息,也就是说,媒体类型是数据契约。如果您传递 application/xml 或 application/json,那么就无法向客户端传达有关 XML/Json 中包含内容的任何信息。一旦客户端应用程序读取并提取 /Customer/Name,您就会创建基于不在消息中的信息的耦合。消除带外耦合对实现 REST 的成功至关重要。 - Darrel Miller
6
客户端不应该事先知道API的URL,除了根URL之外。你不应该将表示格式与特定的URL捆绑在一起。在选择媒体类型时,你需要在特定格式(例如 application/vnd.mycompany.myformat+xml)和标准化格式(例如 XHtml、Atom、RDF 等)之间进行选择。 - Darrel Miller
4
把API版本放在单独的头部字段中有意义吗?比如:Accept: application/com.example.myapp+json; version=1.0。 - Erik
显示剩余20条评论

35

我认为将其作为URI本身的一部分(选项1)是最好的选择,因为v4标识的是与v3不同的资源。像您第二个选项中的查询参数可以最好地用于传递与请求相关的其他(查询)信息,而不是资源


12
问题是,我们讨论的是不同的“资源”,还是该资源的不同表达方式?REST 是否区分资源和表示之间的差异? - Cheeso
1
@Cheeso - OP 表示这是一种不同的表示方式而不是不同的资源,因此我的答案是这样的。 - Greg Beech
这个问题之前在这里有更详细的回答:https://dev59.com/l3RC5IYBdhLWcg3wK9yV。 - Taras Alenin
“像第二个选项中的查询参数可以最好地用于传递与请求相关的附加(查询)信息,而不是资源。” - andy
对于不同的表示形式,我认为你应该使用像“Accept”这样的标头,然后客户端可以指定给服务器“我只接受版本4”,服务器可以用该表示形式进行回答。如果没有发送接受,则提供最后一个版本。 - Carlos Verdes

22

啊,我又戴上了我的老脾气帽子。

从ReST的角度来看,这一点都不重要。毫无意义。

客户端接收到一个它想要跟随的URI,并将其视为不透明的字符串。在其中放置任何你想要的内容,客户端对版本标识符一无所知。

客户端知道的是它可以处理媒体类型,我会建议遵循达雷尔的建议。而且我个人认为,在restful架构中需要4次更改使用的格式,应该引起巨大的警示,你正在做一些非常错误的事情,并完全绕过了为变化弹性设计你的媒体类型的需求。

但无论哪种方式,客户端只能处理它可以理解的格式的文档,并在其中跟随链接。它应该知道链接关系(转换)。因此,URI中的内容完全无关紧要。

我个人会投票支持http://localhost/3f3405d5-5984-4683-bf26-aca186d21c04

这是一个完全有效的标识符,将防止任何进一步的客户端开发人员或操作此系统的人质疑是否应该在URI的开头或末尾放置v4(我建议从服务器的角度来看,你不应该有4个版本,而应该有4个媒体类型)。


如果表示方式需要显著改变并且不兼容旧版本,该怎么办? - Mike Pone
1
通过以可扩展的方式设计您的媒体类型,例如使用命名空间和可扩展的XSD,或现有的XML格式(如Atom),这应该是可以预防的。如果您真的必须这样做,另一种媒体类型是可行的选择。 - SerialSeb
1
我喜欢这个完全有效的答案,但我认为所提议的URI更多是为了证明观点而不是在实际场景中需要“可黑客化”的URI。 - Dave Van den Eynde

7

14
抱歉,但我认为你不会得到像这样可笑的URL。你把版本号与特定资源或(更糟糕的是)特定表示绑定在一起,这种做法是愚蠢的,我的看法是这样的。相反,你应该对API进行版本控制,所以URI中永远不会有多个版本。 - fool4jesus

5

API的版本控制有四种不同的方法:

  • Adding version to the URI path:

    http://example.com/api/v1/foo
    
    http://example.com/api/v2/foo
    

    When you have breaking change, you must increment the version like: v1, v2, v3...

    You can implement a controller in you code like this:

    @RestController
    public class FooVersioningController {
    
    @GetMapping("v1/foo")
    public FooV1 fooV1() {
        return new FooV1("firstname lastname");
    }
    
    @GetMapping("v2/foo")
    public FooV2 fooV2() {
        return new FooV2(new Name("firstname", "lastname"));
    }
    
  • Request parameter versioning:

    http://example.com/api/v2/foo/param?version=1
    http://example.com/api/v2/foo/param?version=2
    

    The version parameter can be optional or required depending on how you want the API to be used.

    The implementation can be similar to this:

    @GetMapping(value = "/foo/param", params = "version=1")
    public FooV1 paramV1() {
        return new FooV1("firstname lastname");
    }
    
    @GetMapping(value = "/foo/param", params = "version=2")
    public FooV2 paramV2() {
        return new FooV2(new Name("firstname", "lastname"));
    }
    
  • Passing a custom header:

    http://localhost:8080/foo/produces
    

    With header:

    headers[Accept=application/vnd.company.app-v1+json]
    

    or:

    headers[Accept=application/vnd.company.app-v2+json]
    

    Largest advantage of this scheme is mostly semantics: You aren’t cluttering the URI with anything to do with the versioning.

    Possible implementation:

    @GetMapping(value = "/foo/produces", produces = "application/vnd.company.app-v1+json")
    public FooV1 producesV1() {
        return new FooV1("firstname lastname");
    }
    
    @GetMapping(value = "/foo/produces", produces = "application/vnd.company.app-v2+json")
    public FooV2 producesV2() {
        return new FooV2(new Name("firstname", "lastname"));
    }
    
  • Changing Hostnames or using API Gateways:

    Essentially, you’re moving the API from one hostname to another. You might even just call this building a new API to the same resources.

    Also,you can do this using API Gateways.


5

3

2
如果您使用URI进行版本控制,则版本号应该在API根的URI中,以便每个资源标识符都可以包含它。
从技术上讲,REST API不会因为URL更改而中断(这是统一接口约束的结果)。它只有在相关语义(例如API特定的RDF词汇)以一种非向后兼容的方式发生更改时才会中断(很少见)。目前,许多人不使用链接进行导航(HATEOAS约束),也不使用词汇来注释其REST响应(自描述消息约束),这就是为什么它们的客户端会中断的原因。
自定义MIME类型和MIME类型版本控制并没有帮助,因为将相关元数据和表示结构放入短字符串中不起作用。当然,元数据和结构经常会发生变化,因此版本号也会随之改变...
所以,回答您的问题,最好的方法是使用词汇(Hydralinked data)对您的请求和响应进行注释,并忘记版本控制或仅在非向后兼容的词汇更改时使用它(例如,如果您想要用另一个词汇替换一个词汇)。

2
如果REST服务在使用前需要进行身份验证,您可以轻松地将API密钥/令牌与API版本关联并在内部进行路由。要使用新的API版本,可能需要新的API密钥,与该版本相关联。
不幸的是,这种解决方案只适用于基于认证的API。然而,它确实可以使版本号从URI中剥离出来。

1
我会在URI的末尾包含版本作为可选值。这可以是后缀,例如/V4,也可以是查询参数,就像您所描述的那样。您甚至可以将/V4重定向到查询参数,以便支持两种变体。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接