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

Aug*_*sto 107 java versioning rest spring spring-mvc

我一直在寻找如何使用Spring 3.2.x管理REST API版本,但我找不到任何易于维护的东西.我先解释一下我遇到的问题,然后解决一个问题...但我想知道我是否在这里重新发明了这个问题.

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

因此,采用版本1.0到1.8的API,其中版本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;
}
Run Code Online (Sandbox Code Playgroud)

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

现在,由于上面的内容是不可能的,如果没有对Spring的工作方式进行认真的修改,我就会考虑修改处理程序与请求匹配的方式,特别是编写我自己的方式ProducesRequestCondition,并在那里有版本范围.例如

码:

@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;
}
Run Code Online (Sandbox Code Playgroud)

通过这种方式,我可以在注释的产生部分中定义关闭或打开的版本范围.我工作的这个解决方案现在,随着我仍然不得不更换一些核心Spring MVC类(的问题RequestMappingInfoHandlerMapping,RequestMappingHandlerMappingRequestMappingInfo),我不喜欢,因为这意味着额外的工作,每当我决定升级到较新版本弹簧.

我会很感激任何想法......特别是,任何建议都可以用更简单,更容易维护的方式来做到这一点.


编辑

添加赏金.为了得到赏金,请回答上面的问题,而不建议在控制器本身中使用这个逻辑.Spring已经有很多逻辑来选择调用哪个控制器方法,我想捎带它.


编辑2

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

xwo*_*ker 60

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

如何在不在方法体中进行评估的情况下执行自定义请求映射,从而对请求中的标头值进行任意评估?

本SO答案中所述,您实际上可以@RequestMapping使用相同的内容并使用不同的注释来区分在运行时期间发生的实际路由.为此,您必须:

  1. 创建新注释VersionRange.
  2. 实施一个RequestCondition<VersionRange>.由于您将拥有类似最佳匹配算法的内容,因此您必须检查使用其他VersionRange值注释的方法是否为当前请求提供了更好的匹配.
  3. 实现一个VersionRangeRequestMappingHandlerMapping基于注释和请求条件(如职位描述如何实现@RequestMapping自定义属性 ).
  4. 配置弹簧以VersionRangeRequestMappingHandlerMapping在使用默认值之前评估您的RequestMappingHandlerMapping(例如,通过将其顺序设置为0).

这不需要任何Spring组件的hacky替换,但使用Spring配置和扩展机制,因此即使您更新Spring版本它也应该工作(只要新版本支持这些机制).

  • 不幸的是,这对 Swagger 来说并不好用,因为在扩展 WebMvcConfigurationSupport 时很多自动配置都被关闭了。 (2认同)

Ben*_*n M 46

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

例:

@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

}
Run Code Online (Sandbox Code Playgroud)

执行:

ApiVersion.java注释:

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

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);
    }

}
Run Code Online (Sandbox Code Playgroud)

注入WebMvcConfigurationSupport:

public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        return new ApiVersionRequestMappingHandlerMapping("v");
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我将int []更改为String []以允许类似"1.2"的版本,因此我可以处理像"latest"这样的关键字 (4认同)
  • 如果你想将自己的`RequestMappingHandlerMapping`注入你的`WebMvcConfiguration`,你应该覆盖`createRequestMappingHandlerMapping`而不是`requestMappingHandlerMapping`!否则你会遇到奇怪的问题(由于关闭的会话,我突然遇到了Hibernates延迟初始化的问题) (4认同)
  • 是的,这很合理.对于未来的项目,由于某些原因,我会采用不同的方式:**1.**URL代表资源.`/ v1/aResource`和`/ v2/aResource`看起来像不同的资源,**但**它只是同一资源的不同表示!**2.**使用HTTP标题看起来更好,**但**你不能给某人一个URL,因为URL不包含标题.**3.**使用URL参数,即`/ aResource?v = 2.1`(顺便说一下:这就是Google进行版本控制的方式).`...`我仍然不确定我是否选择*2*或*3*,但由于上述原因,我再也不会使用*1*. (3认同)
  • 有一种方法可以使这个工作,不扩展`WebMvcConfigurationSupport`而是扩展`DelegatingWebMvcConfiguration`。这对我有用(见 /sf/ask/1558703401/) (2认同)

elu*_*ode 16

我仍然建议使用URL进行版本控制,因为在URL中,@ RequestMapping支持模式和路径参数,可以使用regexp指定哪种格式.

要处理客户端升级(您在评论中提到),您可以使用"最新"之类的别名.或者使用最新版本的api的无版本版本(是的).

同样使用路径参数,您可以实现任何复杂的版本处理逻辑,如果您已经想要有范围,那么您很可能想要更快的东西.

以下是几个例子:

@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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

例如,您可以拥有一个仅包含版本处理方法的控制器.

在该处理中,您可以在某些弹簧服务/组件中查找(使用反射/ AOP /代码生成库),或者在同一类中查找具有相同名称/签名且需要@VersionRange的方法,并调用它传递所有参数.


msp*_*ant 12

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

一般来说,有三种主要的休息版本方法:

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

    http://localhost:9001/api/v1/user
    http://localhost:9001/api/v2/user
    
    Run Code Online (Sandbox Code Playgroud)
  • Content-Type标头,客户端在Accept标头中定义版本:

    http://localhost:9001/api/v1/user with 
    Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json
    
    Run Code Online (Sandbox Code Playgroud)
  • 自定义标头,客户端在自定义标头中定义版本.

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

问题第二个做法是,一些工具,如http://swagger.io/具有相同的路径,但不同的Content-Type的操作之间不能明显(检查问题https://github.com/OAI/OpenAPI-Specification/issues/ 146)

解决方案

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

假设我们有用户控制器的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";
    }
 }
Run Code Online (Sandbox Code Playgroud)

要求是,如果我请求V1为用户资源我必须采取"用户V1" repsonse,否则如果我请求V2,V3等我必须采取"用户V2"响应.

在此输入图像描述

为了在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 ;
    }
Run Code Online (Sandbox Code Playgroud)

}

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

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

http://localhost:9001/api/v{version}/user
http://localhost:9001/api/v{version}/company
Run Code Online (Sandbox Code Playgroud)

假设我们改变了打破客户的公司"合同".所以我们实现了http://localhost:9001/api/v2/company,我们要求客户在v1上改为v2.

所以客户的新请求是:

http://localhost:9001/api/v2/user
http://localhost:9001/api/v2/company
Run Code Online (Sandbox Code Playgroud)

代替:

http://localhost:9001/api/v1/user
http://localhost:9001/api/v1/company
Run Code Online (Sandbox Code Playgroud)

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

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

在此输入图像描述

GIT

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


Wil*_*ler 8

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

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

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

我不认为我之前实际使用过这个元素(如果我不记得的话),所以我只是在文档中删除

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

  • 我知道这一点,但正如你所说,在每个版本上我都需要去我的所有控制器并添加一个版本,即使它们没有改变.您提到的范围仅适用于完整类型,例如`application/*`而不是类型的部分.例如,Spring中的以下内容无效:"Accept = application/vnd.company.app-1.*+ json"`.这与spring类`MediaType`的工作方式有关 (2认同)

Dhe*_*rik 7

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

\n
/api/v1/orders\n/api/v2/orders\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n
\n

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

\n
\n

Spring上的实现

\n

首先,您创建一个控制器produces属性的控制器,该属性默认应用于同一类内的每个端点。

\n
@RestController\n@RequestMapping(value = "/api/orders/", produces = "application/vnd.company.etc.v1+json")\npublic class OrderController {\n\n}\n
Run Code Online (Sandbox Code Playgroud)\n

之后,我们可以想象一个可能的场景,您有两个版本(v1v2)的端点用于“创建订单”:

\n
@Deprecated\n@PostMapping\npublic ResponseEntity<OrderResponse> createV1(\n        @RequestBody OrderRequest orderRequest) {\n\n    OrderResponse response = createOrderService.createOrder(orderRequest);\n    return new ResponseEntity<>(response, HttpStatus.CREATED);\n}\n\n@PostMapping(\n        produces = "application/vnd.company.etc.v2+json",\n        consumes = "application/vnd.company.etc.v2+json")\npublic ResponseEntity<OrderResponseV2> createV2(\n        @RequestBody OrderRequestV2 orderRequest) {\n\n    OrderResponse response = createOrderService.createOrder(orderRequest);\n    return new ResponseEntity<>(response, HttpStatus.CREATED);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

完毕!只需使用所需的Http 标头调用每个端点

\n
Content-Type: application/vnd.company.etc.v1+json\n
Run Code Online (Sandbox Code Playgroud)\n

或者,调用 v2:

\n
Content-Type: application/vnd.company.etc.v2+json\n
Run Code Online (Sandbox Code Playgroud)\n

关于您的担忧:

\n
\n

由于并非 API 中的所有方法都在同一版本中发生变化,因此我不想转到每个控制器并为版本之间未更改的处理程序更改任何内容

\n
\n

正如所解释的,该策略使用其实际版本维护每个控制器和端点。您仅修改已修改且需要新版本的端点。

\n

还有大摇大摆的?

\n

使用此策略设置不同版本的 Swagger 也非常容易。看这个答案以了解更多详细信息。

\n


归档时间:

查看次数:

66784 次

最近记录:

6 年,5 月 前