Java 如何使用 spring 管理 REST API 版本控制?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/20198275/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-13 00:12:20  来源:igfitidea点击:

How to manage REST API versioning with spring?

javaspringrestspring-mvcversioning

提问by Augusto

I've been searching how to manage a REST API versions using Spring 3.2.x, but I haven't find anything that is easy to maintain. I'll explain first the problem I have, and then a solution... but I do wonder if I'm re-inventing the wheel here.

我一直在寻找如何使用 Spring 3.2.x 管理 REST API 版本,但我没有找到任何易于维护的东西。我将首先解释我遇到的问题,然后是解决方案……但我确实想知道我是否在这里重新发明轮子。

I want to manage the version based on the Accept header, and for example if a request has the Accept header application/vnd.company.app-1.1+json, I want spring MVC to forward this to the method that handles this version. And since not all methods in an API change in the same release, I don't want to go to each of my controllers and change anything for a handler that hasn't changed between versions. I also don't want to have the logic to figure out which version to use in the controller themselves (using service locators) as Spring is already discovering which method to call.

我想根据 Accept 标头管理版本,例如,如果请求具有 Accept 标头application/vnd.company.app-1.1+json,我希望 spring MVC 将其转发给处理此版本的方法。而且由于并非同一版本中 API 中的所有方法都会更改,因此我不想去我的每个控制器并为版本之间没有更改的处理程序更改任何内容。我也不希望有逻辑来确定在控制器中使用哪个版本(使用服务定位器),因为 Spring 已经在发现要调用的方法。

So taken an API with versions 1.0, to 1.8 where a handler was introduced in version 1.0 and modified in v1.7, I would like handle this in the following way. Imagine that the code is inside a controller, and that there's some code that is able to extract the version from the header. (The following is invalid in Spring)

因此,将 1.0 版的 API 带到 1.8 版,其中在 1.0 版中引入了处理程序并在 v1.7 中进行了修改,我想通过以下方式处理此问题。想象一下,代码在一个控制器中,并且有一些代码能够从头文件中提取版本。(以下在Spring中无效)

@RequestMapping(...)
@VersionRange(1.0,1.6)
@ResponseBody
public Object method1() {
   // so something
   return object;
}

@RequestMapping(...) //same Request mapping annotation
@VersionRange(1.7)
@ResponseBody
public Object method2() {
   // so something
   return object;
}

This is not possible in spring as the 2 methods have the same RequestMappingannotation and Spring fails to load. The idea is that the VersionRangeannotation can define an open or closed version range. The first method is valid from versions 1.0 to 1.6, while the second for version 1.7 onwards (including the latest version 1.8). I know that this approach breaks if someone decides to pass version 99.99, but that's something I'm OK to live with.

这在 spring 中是不可能的,因为这 2 种方法具有相同的RequestMapping注释并且 Spring 无法加载。这个想法是VersionRange注释可以定义一个开放或封闭的版本范围。第一种方法适用于 1.0 到 1.6 版本,第二种方法适用于 1.7 版本(包括最新版本 1.8)。我知道如果有人决定通过 99.99 版,这种方法就会中断,但我可以接受。

Now, since the above is not possible without a serious rework of how spring works, I was thinking of tinkering with the way handlers matched to requests, in particular to write my own ProducesRequestCondition, and have the version range in there. For example

现在,由于如果不对 spring 的工作方式进行认真的返工,上述内容是不可能的,我正在考虑修改处理程序与请求匹配的方式,特别是编写我自己的ProducesRequestCondition,并在其中包含版本范围。例如

Code:

代码:

@RequestMapping(..., produces = "application/vnd.company.app-[1.0-1.6]+json)
@ResponseBody
public Object method1() {
   // so something
   return object;
}

@RequestMapping(..., produces = "application/vnd.company.app-[1.7-]+json)
@ResponseBody
public Object method2() {
   // so something
   return object;
}

In this way, I can have closed or open version ranges defined in the produces part of the annotation. I'm working on this solution now, with the problem that I still had to replace some core Spring MVC classes (RequestMappingInfoHandlerMapping, RequestMappingHandlerMappingand RequestMappingInfo), which I don't like, because it means extra work whenever I decide to upgrade to a newer version of spring.

通过这种方式,我可以在注释的生产部分中定义封闭或开放的版本范围。我现在正在研究这个解决方案,但问题是我仍然需要替换一些我不喜欢的核心 Spring MVC 类(RequestMappingInfoHandlerMapping,RequestMappingHandlerMappingRequestMappingInfo),因为每当我决定升级到更新版本的时候,这意味着额外的工作春天。

I would appreciate any thoughts... and especially, any suggestion to do this in a simpler, easier to maintain way.

我很感激任何想法......尤其是,任何以更简单,更易于维护的方式执行此操作的建议。



Edit

编辑

Adding a bounty. To get the bounty, please answer the question above without suggesting to have this logic in the controller themselves. Spring already has a lot of logic to select which controller method to call, and I want to piggyback on that.

添加赏金。要获得赏金,请回答上述问题,而不建议在控制器本身中使用此逻辑。Spring 已经有很多逻辑来选择要调用的控制器方法,我想利用它。



Edit 2

编辑 2

I've shared the original POC (with some improvements) in github: https://github.com/augusto/restVersioning

我在 github 中分享了原始 POC(有一些改进):https: //github.com/augusto/restVersioning

采纳答案by xwoker

Regardless whether versioning can be avoided by doing backwards compatible changes (which might not always possible when you are bound by some corporate guidelines or your API clients are implemented in a buggy way and would break even if they should not) the abstracted requirement is an interesting one:

无论是否可以通过向后兼容的更改来避免版本控制(当您受到某些公司指南的约束或您的 API 客户端以错误的方式实现并且即使不应该也会中断)时,这可能并不总是可行的,抽象的需求是一个有趣的一:

How can I do a custom request mapping that does arbitrary evaluations of header values from the request without doing the evaluation in the method body?

如何进行自定义请求映射,对请求中的标头值进行任意评估,而无需在方法主体中进行评估?

As described in this SO answeryou actually can have the same @RequestMappingand use a different annotation to differentiate during the actual routing that happens during runtime. To do so, you will have to:

this SO answer中所述,您实际上可以@RequestMapping使用相同的注释并使用不同的注释来区分运行时发生的实际路由。为此,您必须:

  1. Create a new annotation VersionRange.
  2. Implement a RequestCondition<VersionRange>. Since you will have something like a best-match algorithm you will have to check whether methods annotated with other VersionRangevalues provide a better match for the current request.
  3. Implement a VersionRangeRequestMappingHandlerMappingbased on the annotation and request condition (as described in the postHow to implement @RequestMapping custom properties ).
  4. Configure spring to evaluate your VersionRangeRequestMappingHandlerMappingbefore using the default RequestMappingHandlerMapping(e.g. by setting its order to 0).
  1. 创建新注释VersionRange
  2. 实施一个RequestCondition<VersionRange>. 由于您将拥有诸如最佳匹配算法之类的东西,因此您必须检查使用其他VersionRange值注释的方法是否为当前请求提供了更好的匹配。
  3. VersionRangeRequestMappingHandlerMapping根据注解和请求条件实现一个(如如何实现@RequestMapping 自定义属性一文中所述 )。
  4. VersionRangeRequestMappingHandlerMapping在使用默认值之前配置 spring 以评估您的值RequestMappingHandlerMapping(例如,将其顺序​​设置为 0)。

This wouldn't require any hacky replacements of Spring components but uses the Spring configuration and extension mechanisms so it should work even if you update your Spring version (as long as the new version supports these mechanisms).

这不需要对 Spring 组件进行任何 hacky 替换,而是使用 Spring 配置和扩展机制,因此即使您更新 Spring 版本它也应该可以工作(只要新版本支持这些机制)。

回答by codesalsa

In produces you can have negation. So for method1 say produces="!...1.7"and in method2 have the positive.

在生产中,你可以有否定。所以对于method1来说produces="!...1.7",在method2中有积极的一面。

The produces is also an array so you for method1 you can say produces={"...1.6","!...1.7","...1.8"}etc (accept all except 1.7)

产品也是一个数组,所以你可以对 method1 说produces={"...1.6","!...1.7","...1.8"}等(接受除 1.7 之外的所有内容)

Ofcourse not as ideal as ranges that you have in mind but I think easier to maintain than other custom stuff if this is something uncommon in your system. Good luck!

当然,不像您想象的范围那么理想,但我认为如果这在您的系统中不常见,那么比其他自定义内容更容易维护。祝你好运!

回答by elusive-code

I would still recommend using URL's for versioning because in URLs @RequestMapping supports patterns and path parameters, which format could be specified with regexp.

我仍然建议使用 URL 进行版本控制,因为在 URL 中 @RequestMapping 支持模式和路径参数,可以使用正则表达式指定格式。

And to handle client upgrades (which you mentioned in comment) you can use aliases like 'latest'. Or have unversioned version of api which uses latest version (yeah).

并且要处理客户端升级(您在评论中提到的),您可以使用诸如“最新”之类的别名。或者有使用最新版本的 api 的未版本化版本(是的)。

Also using path parameters you can implement any complex version handling logic, and if you already want to have ranges, you very well might want something more soon enough.

同样使用路径参数,您可以实现任何复杂的版本处理逻辑,如果您已经想要范围,您很可能很快就会想要一些东西。

Here is a couple of examples:

下面是几个例子:

@RequestMapping({
    "/**/public_api/1.1/method",
    "/**/public_api/1.2/method",
})
public void method1(){
}

@RequestMapping({
    "/**/public_api/1.3/method"
    "/**/public_api/latest/method"
    "/**/public_api/method" 
})
public void method2(){
}

@RequestMapping({
    "/**/public_api/1.4/method"
    "/**/public_api/beta/method"
})
public void method2(){
}

//handles all 1.* requests
@RequestMapping({
    "/**/public_api/{version:1\.\d+}/method"
})
public void methodManual1(@PathVariable("version") String version){
}

//handles 1.0-1.6 range, but somewhat ugly
@RequestMapping({
    "/**/public_api/{version:1\.[0123456]?}/method"
})
public void methodManual1(@PathVariable("version") String version){
}

//fully manual version handling
@RequestMapping({
    "/**/public_api/{version}/method"
})
public void methodManual2(@PathVariable("version") String version){
    int[] versionParts = getVersionParts(version);
    //manual handling of versions
}

public int[] getVersionParts(String version){
    try{
        String[] versionParts = version.split("\.");
        int[] result = new int[versionParts.length];
        for(int i=0;i<versionParts.length;i++){
            result[i] = Integer.parseInt(versionParts[i]);
        }
        return result;
    }catch (Exception ex) {
        return null;
    }
}

Based on the last approach you can actually implement something like what you want.

基于最后一种方法,您实际上可以实现您想要的东西。

For example you can have a controller that contains only method stabs with version handling.

例如,您可以拥有一个仅包含具有版本处理功能的方法 stabs 的控制器。

In that handling you look (using reflection/AOP/code generation libraries) in some spring service/component or in the same class for method with the same name/signature and required @VersionRange and invoke it passing all parameters.

在该处理中,您查看(使用反射/AOP/代码生成库)在某些 spring 服务/组件或同一类中具有相同名称/签名和所需 @VersionRange 的方法,并调用它传递所有参数。

回答by elusive-code

The @RequestMappingannotation supports a headerselement that allows you to narrow the matching requests. In particular you can use the Acceptheader here.

@RequestMapping注释支持headers元素,使您可以缩小匹配的请求。特别是您可以在Accept此处使用标题。

@RequestMapping(headers = {
    "Accept=application/vnd.company.app-1.0+json",
    "Accept=application/vnd.company.app-1.1+json"
})

This isn't exactly what you're describing, since it doesn't directly handle ranges, but the element does support the * wildcard as well as !=. So at least you could get away with using a wildcard for cases where all versions support the endpoint in question, or even all minor versions of a given major version (e.g. 1.*).

这与您所描述的不完全相同,因为它不直接处理范围,但该元素确实支持 * 通配符以及 !=。因此,至少您可以在所有版本都支持相关端点的情况下使用通配符,甚至是给定主要版本的所有次要版本(例如 1.*)。

I don't think I've actually used this element before (if I have I don't remember), so I'm just going off the documentation at

我不认为我以前真的使用过这个元素(如果我不记得了),所以我只是离开文档

http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestMapping.html

http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestMapping.html

回答by hevi

You can use AOP, around interception

可以使用AOP,绕过拦截

Consider having a request mapping which receives all the /**/public_api/*and in this method do nothing;

考虑让一个请求映射接收所有,/**/public_api/*并且在这个方法中什么都不做;

@RequestMapping({
    "/**/public_api/*"
})
public void method2(Model model){
}

After

@Override
public void around(Method method, Object[] args, Object target)
    throws Throwable {
       // look for the requested version from model parameter, call it desired range
       // check the target object for @VersionRange annotation with reflection and acquire version ranges, call the function if it is in the desired range


}

The only constraint is that all has to be in the same controller.

唯一的限制是所有的都必须在同一个控制器中。

For AOP configuration have a look at http://www.mkyong.com/spring/spring-aop-examples-advice/

对于 AOP 配置,查看http://www.mkyong.com/spring/spring-aop-examples-advice/

回答by Benjamin M

I just created a custom solution. I'm using the @ApiVersionannotation in combination with @RequestMappingannotation inside @Controllerclasses.

我刚刚创建了一个自定义解决方案。我使用的是@ApiVersion与组合注解@RequestMapping注释内部@Controller类。

Example:

例子:

@Controller
@RequestMapping("x")
@ApiVersion(1)
class MyController {

    @RequestMapping("a")
    void a() {}         // maps to /v1/x/a

    @RequestMapping("b")
    @ApiVersion(2)
    void b() {}         // maps to /v2/x/b

    @RequestMapping("c")
    @ApiVersion({1,3})
    void c() {}         // maps to /v1/x/c
                        //  and to /v3/x/c

}

Implementation:

执行:

ApiVersion.javaannotation:

ApiVersion.java注释:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
    int[] value();
}

ApiVersionRequestMappingHandlerMapping.java(this is mostly copy and paste from RequestMappingHandlerMapping):

ApiVersionRequestMappingHandlerMapping.java(这主要是从 复制和粘贴RequestMappingHandlerMapping):

public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    private final String prefix;

    public ApiVersionRequestMappingHandlerMapping(String prefix) {
        this.prefix = prefix;
    }

    @Override
    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        RequestMappingInfo info = super.getMappingForMethod(method, handlerType);
        if(info == null) return null;

        ApiVersion methodAnnotation = AnnotationUtils.findAnnotation(method, ApiVersion.class);
        if(methodAnnotation != null) {
            RequestCondition<?> methodCondition = getCustomMethodCondition(method);
            // Concatenate our ApiVersion with the usual request mapping
            info = createApiVersionInfo(methodAnnotation, methodCondition).combine(info);
        } else {
            ApiVersion typeAnnotation = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
            if(typeAnnotation != null) {
                RequestCondition<?> typeCondition = getCustomTypeCondition(handlerType);
                // Concatenate our ApiVersion with the usual request mapping
                info = createApiVersionInfo(typeAnnotation, typeCondition).combine(info);
            }
        }

        return info;
    }

    private RequestMappingInfo createApiVersionInfo(ApiVersion annotation, RequestCondition<?> customCondition) {
        int[] values = annotation.value();
        String[] patterns = new String[values.length];
        for(int i=0; i<values.length; i++) {
            // Build the URL prefix
            patterns[i] = prefix+values[i]; 
        }

        return new RequestMappingInfo(
                new PatternsRequestCondition(patterns, getUrlPathHelper(), getPathMatcher(), useSuffixPatternMatch(), useTrailingSlashMatch(), getFileExtensions()),
                new RequestMethodsRequestCondition(),
                new ParamsRequestCondition(),
                new HeadersRequestCondition(),
                new ConsumesRequestCondition(),
                new ProducesRequestCondition(),
                customCondition);
    }

}

Injection into WebMvcConfigurationSupport:

注入 WebMvcConfigurationSupport:

public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        return new ApiVersionRequestMappingHandlerMapping("v");
    }
}

回答by Ceekay

What about just using inheritance to model versioning? That is what I'm using in my project and it requires no special spring configuration and gets me exactly what I want.

仅使用继承来建模版本控制怎么样?这就是我在我的项目中使用的,它不需要特殊的 spring 配置,就能得到我想要的东西。

@RestController
@RequestMapping(value = "/test/1")
@Deprecated
public class Test1 {
...Fields Getters Setters...
    @RequestMapping(method = RequestMethod.GET)
    @Deprecated
    public Test getTest(Long id) {
        return serviceClass.getTestById(id);
    }
    @RequestMapping(method = RequestMethod.PUT)
    public Test getTest(Test test) {
        return serviceClass.updateTest(test);
    }

}

@RestController
@RequestMapping(value = "/test/2")
public class Test2 extends Test1 {
...Fields Getters Setters...
    @Override
    @RequestMapping(method = RequestMethod.GET)
    public Test getTest(Long id) {
        return serviceClass.getAUpdated(id);
    }

    @RequestMapping(method = RequestMethod.DELETE)
    public Test deleteTest(Long id) {
        return serviceClass.deleteTestById(id);
    }
}

This set up allows for little duplication of code and the ability to overwrite methods into new versions of the api with little work. It also saves the need to complicate your source code with version switching logic. If you don't code an endpoint in a version it will grab the previous version by default.

这种设置允许很少的代码重复,并且能够以很少的工作将方法覆盖到新版本的 api 中。它还避免了使用版本切换逻辑使源代码复杂化的需要。如果您不在某个版本中对端点进行编码,则默认情况下它将获取以前的版本。

Compared to what others are doing this seems way easier. Is there something I'm missing?

与其他人所做的相比,这似乎更容易。有什么我想念的吗?

回答by mspapant

I have implemented a solution which handles PERFECTLYthe problem with rest versioning.

我已经实现了一个解决方案,它完美地处理了休息版本控制的问题。

General Speaking there are 3 major approaches for rest versioning:

一般而言,rest 版本控制有 3 种主要方法:

  • Path-based approch, in which the client defines the version in URL:

    http://localhost:9001/api/v1/user
    http://localhost:9001/api/v2/user
    
  • Content-Typeheader, in which the client defines the version in Acceptheader:

    http://localhost:9001/api/v1/user with 
    Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json
    
  • Custom Header, in which the client defines the version in a custom header.

  • 基于路径的方法,其中客户端在 URL 中定义版本:

    http://localhost:9001/api/v1/user
    http://localhost:9001/api/v2/user
    
  • Content-Type头,其中客户端在Accept头中定义版本:

    http://localhost:9001/api/v1/user with 
    Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json
    
  • 自定义 Header,其中客户端在自定义标题中定义版本。

The problemwith the firstapproach is that if you change the version let's say from v1 -> v2, probably you need to copy-paste the v1 resources that haven't changed to v2 path

问题第一个方法是,如果你改变的版本,让我们从V1说- > V2,也许你需要复制,粘贴并没有改变,以V2路径V1资源

The problemwith the secondapproach is that some tools like http://swagger.io/cannot distinct between operations with same path but different Content-Type (check issue https://github.com/OAI/OpenAPI-Specification/issues/146)

问题第二个做法是,一些工具,如http://swagger.io/可以用相同的路径,但不同的内容类型(支票发放操作之间没有明显的https://github.com/OAI/OpenAPI-Specification/issues/ 146)

The solution

解决方案

Since i am working a lot with rest documentation tools, i prefer to use the first approach. My solution handles the problemwith the first approach, so you don't need to copy-paste the endpoint to the new version.

由于我经常使用休息文档工具,因此我更喜欢使用第一种方法。我的解决方案使用第一种方法处理问题,因此您无需将端点复制粘贴到新版本。

Let's say we have v1 and v2 versions for the User controller:

假设我们有用户控制器的 v1 和 v2 版本:

package com.mspapant.example.restVersion.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * The user controller.
 *
 * @author : Manos Papantonakos on 19/8/2016.
 */
@Controller
@Api(value = "user", description = "Operations about users")
public class UserController {

    /**
     * Return the user.
     *
     * @return the user
     */
    @ResponseBody
    @RequestMapping(method = RequestMethod.GET, value = "/api/v1/user")
    @ApiOperation(value = "Returns user", notes = "Returns the user", tags = {"GET", "User"})
    public String getUserV1() {
         return "User V1";
    }

    /**
     * Return the user.
     *
     * @return the user
     */
    @ResponseBody
    @RequestMapping(method = RequestMethod.GET, value = "/api/v2/user")
    @ApiOperation(value = "Returns user", notes = "Returns the user", tags = {"GET", "User"})
    public String getUserV2() {
         return "User V2";
    }
 }

The requirementis if i request the v1for the user resource i have to take the "User V1"repsonse, otherwise if i request the v2, v3and so on i have to take the "User V2"response.

要求是,如果我请求V1为用户资源我必须采取“用户V1”repsonse,否则如果我请求V2V3等我必须采取“用户V2”响应。

enter image description here

在此处输入图片说明

In order to implement this in spring, we need to override the default RequestMappingHandlerMappingbehavior:

为了在 spring 中实现这一点,我们需要覆盖默认的RequestMappingHandlerMapping行为:

package com.mspapant.example.restVersion.conf.mapping;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class VersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    @Value("${server.apiContext}")
    private String apiContext;

    @Value("${server.versionContext}")
    private String versionContext;

    @Override
    protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
        HandlerMethod method = super.lookupHandlerMethod(lookupPath, request);
        if (method == null && lookupPath.contains(getApiAndVersionContext())) {
            String afterAPIURL = lookupPath.substring(lookupPath.indexOf(getApiAndVersionContext()) + getApiAndVersionContext().length());
            String version = afterAPIURL.substring(0, afterAPIURL.indexOf("/"));
            String path = afterAPIURL.substring(version.length() + 1);

            int previousVersion = getPreviousVersion(version);
            if (previousVersion != 0) {
                lookupPath = getApiAndVersionContext() + previousVersion + "/" + path;
                final String lookupFinal = lookupPath;
                return lookupHandlerMethod(lookupPath, new HttpServletRequestWrapper(request) {
                    @Override
                    public String getRequestURI() {
                        return lookupFinal;
                    }

                    @Override
                    public String getServletPath() {
                        return lookupFinal;
                    }});
            }
        }
        return method;
    }

    private String getApiAndVersionContext() {
        return "/" + apiContext + "/" + versionContext;
    }

    private int getPreviousVersion(final String version) {
        return new Integer(version) - 1 ;
    }

}

}

The implementation reads the version in the URL and asks from spring to resolve the URL .In case this URL does not exists (for example the client requested v3) then we try with v2and so one until we find the most recent versionfor the resource.

实现读取 URL 中的版本并从 spring 请求解析 URL。如果此 URL 不存在(例如客户端请求v3),那么我们尝试使用v2等,直到我们找到资源的最新版本.

In order to see the benefits from this implementation, let's say we have two resources: User and Company:

为了看到这个实现的好处,假设我们有两个资源:用户和公司:

http://localhost:9001/api/v{version}/user
http://localhost:9001/api/v{version}/company

Let's say we made a change in company "contract" that breaks the client. So we implement the http://localhost:9001/api/v2/companyand we ask from client to change to v2 instead on v1.

假设我们对公司“合同”进行了更改,从而破坏了客户。所以我们实现了http://localhost:9001/api/v2/company,我们要求客户端更改为 v2,而不是在 v1 上。

So the new requests from client are:

所以来自客户端的新请求是:

http://localhost:9001/api/v2/user
http://localhost:9001/api/v2/company

instead of:

代替:

http://localhost:9001/api/v1/user
http://localhost:9001/api/v1/company

The bestpart here is that with this solution the client will get the user information from v1 and company information from v2 without the needto create a new (same) endpoint from user v2!

这里最好的部分是,使用此解决方案,客户端将从 v1 获取用户信息和从 v2获取公司信息,而无需从用户 v2 创建新的(相同)端点!

Rest DocumentationAs i said before the reason i select the URL-based versioning approach is that some tools like swagger do not document differently the endpoints with the same URL but different content type. With this solution, both endpoints are displayed since have different URL:

休息文档正如我之前所说,我选择基于 URL 的版本控制方法的原因是某些工具(如 swagger)不会以不同的方式记录具有相同 URL 但内容类型不同的端点。使用此解决方案,由于具有不同的 URL,因此会显示两个端点:

enter image description here

在此处输入图片说明

GIT

通用电气

Solution implementation at: https://github.com/mspapant/restVersioningExample/

解决方案实现在:https: //github.com/mspapant/restVersioningExample/

回答by Dherik

I already tried to version my API using the URI Versioning, like:

我已经尝试使用URI Versioning对我的 API 进行版本控制,例如:

/api/v1/orders
/api/v2/orders

But there is some challenges when trying to make this work: how organize your code with different versions? How manage two (or more) versions at the same time? What the impact when removing some version?

但是在尝试进行这项工作时存在一些挑战:如何组织不同版本的代码?如何同时管理两个(或更多)版本?删除某些版本有什么影响?

The best alternative that I found was not version the entire API, but control the version on each endpoint. This pattern is called Versioning using Accept headeror Versioning through content negotiation:

我发现的最佳替代方案不是对整个 API进行版本控制,而是控制每个端点上的版本。此模式称为使用 Accept 标头进行版本控制通过内容协商进行版本控制

This approach allows us to version a single resource representation instead of versioning the entire API which gives us a more granular control over versioning. It also creates a smaller footprint in the code base as we don't have to fork the entire application when creating a new version. Another advantage of this approach is that it doesn't require implementing URI routing rules introduced by versioning through the URI path.

这种方法允许我们对单个资源表示进行版本控制,而不是对整个 API 进行版本控制,这使我们可以更精细地控制版本控制。它还在代码库中创建了更小的占用空间,因为我们在创建新版本时不必分叉整个应用程序。这种方法的另一个优点是它不需要通过 URI 路径实现版本控制引入的 URI 路由规则。

Implementation on Spring

Spring上的实现

First, you create a Controller with a basic produces attribute, that will apply by default for each endpoint inside the class.

首先,您创建一个具有基本生产属性的控制器,默认情况下该属性将应用于类中的每个端点。

@RestController
@RequestMapping(value = "/api/orders/", produces = "application/vnd.company.etc.v1+json")
public class OrderController {

}

After that, create a possible scenario where you have two versions of an endpoint for create an order:

之后,创建一个可能的场景,其中您有两个版本的端点来创建订单:

@Deprecated
@PostMapping
public ResponseEntity<OrderResponse> createV1(
        @RequestBody OrderRequest orderRequest) {

    OrderResponse response = createOrderService.createOrder(orderRequest);
    return new ResponseEntity<>(response, HttpStatus.CREATED);
}

@PostMapping(
        produces = "application/vnd.company.etc.v2+json",
        consumes = "application/vnd.company.etc.v2+json")
public ResponseEntity<OrderResponseV2> createV2(
        @RequestBody OrderRequestV2 orderRequest) {

    OrderResponse response = createOrderService.createOrder(orderRequest);
    return new ResponseEntity<>(response, HttpStatus.CREATED);
}

Done! Just call each endpoint using the desired Http Headerversion:

完毕!只需使用所需的Http Header版本调用每个端点:

Content-Type: application/vnd.company.etc.v1+json

Or, to call the version two:

或者,调用版本二:

Content-Type: application/vnd.company.etc.v2+json

About your worries:

关于您的后顾之忧:

And since not all methods in an API change in the same release, I don't want to go to each of my controllers and change anything for a handler that hasn't changed between versions

而且由于并非同一版本中 API 中的所有方法都更改,因此我不想去我的每个控制器并为版本之间没有更改的处理程序更改任何内容

As explained, this strategy maintains each Controller and endpoint with his actual version. You only modify the endpoint that have modifications and needs a new version.

如前所述,此策略使用其实际版本维护每个控制器和端点。您只需修改有修改并需要新版本的端点。

And the Swagger?

而大摇大摆呢?

Setup the Swagger with different versions is also very easy using this strategy. See this answerto more details.

使用此策略设置具有不同版本的 Swagger 也非常容易。有关更多详细信息,请参阅此答案