如何使用spring来pipe理REST API版本?

我一直在寻找如何使用Spring 3.2.xpipe理REST API版本,但是我还没有发现任何易于维护的东西。 我会首先解释我遇到的问题,然后解决scheme…但是我想知道我是否正在重新发明车轮。

我想pipe理基于Accept头的版本,例如,如果请求具有Accept头application/vnd.company.app-1.1+json ,我希望Spring MVC将此转发给处理此版本的方法。 由于不是所有API中的方法都在同一版本中发生变化,我不想去我的每一个控制器,并改变任何版本之间没有改变的处理程序的任何东西。 我也不想让逻辑弄清楚在控制器中使用哪个版本(使用服务定位器),因为Spring已经发现了调用哪个方法。

所以我们把1.0版本的API,1.0版本的1.0版本,1.0版本的修订版本,我想用下面的方法处理。 想象一下,代码是在一个控制器里面,并且有一些代码能够从头文件中提取版本。 (以下在春季无效)

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

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

现在,既然上述是不可能的,如果没有对弹簧的工作方式进行认真的修改,我就会考虑修改处理程序与请求匹配的方式,特别是编写我自己的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; } 

通过这种方式,我可以closures或打开注释生成部分中定义的版本范围。 我现在正在研究这个解决scheme,但仍然需要replace一些我不喜欢的核心Spring MVC类( RequestMappingInfoHandlerMappingRequestMappingHandlerMappingRequestMappingInfo ),因为这意味着每当我决定升级到spring的新版本。

我会很感激任何想法…尤其是,任何build议,以更简单,更容易维护的方式做到这一点。


编辑

添加一个赏金。 要获得赏金,请回答上面的问题,而不是build议在控制器本身有这个逻辑。 Spring已经有了很多的逻辑来select调用哪个控制器方法,并且我想搭载它。


编辑2

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

无论版本控制是否可以通过向后兼容的变更来避免(当您受到某些企业准则的约束,或者您的API客户端以错误的方式执行,并且即使不应该也会中断的情况下,这些更改可能并不总是可行的)抽象的要求是一个有趣的一:

我怎样才能做一个自定义的请求映射,从请求的头值进行任意评估,而不在方法体内进行评估?

正如在这个答案中所描述的,实际上可以有相同的@RequestMapping并使用不同的注释来区分运行期间发生的实际路由。 要做到这一点,你将不得不:

  1. 创build一个新的批注VersionRange
  2. 实现RequestCondition<VersionRange> 。 由于您将拥有最佳匹配algorithm,因此您将需要检查使用其他VersionRange值注释的方法是否为当前请求提供了更好的匹配。
  3. 根据注释和请求条件实现VersionRangeRequestMappingHandlerMapping (如链接文章中所述 )。
  4. configurationspring以在使用默认的RequestMappingHandlerMapping之前评估您的VersionRangeRequestMappingHandlerMapping (例如,将其顺序​​设置为0)。

这将不需要任何疯狂的Spring组件replace,但使用Springconfiguration和扩展机制,即使你更新你的Spring版本(只要新版本支持这些机制)它也应该工作。

我刚创build了一个自定义解决scheme 我在@Controller类中使用了@ApiVersion注释和@RequestMapping注解。

例:

 @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 } 

执行:

ApiVersion.java注释:

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

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

注入WebMvcConfigurationSupport:

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

我仍然build议使用URL进行版本控制,因为在URL中,@RequestMapping支持模式和path参数,可以使用正则expression式指定哪种格式。

而处理客户端升级(你在评论中提到),你可以使用像'最新'的别名。 或者有未版本的使用最新版本的API(是)。

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

这里有几个例子:

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

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

例如,你可以有一个控制器只包含方法刺与版本处理。

在这个处理中,你可以在一些spring服务/组件或同一个类中使用reflection/ AOP /代码生成库来查找具有相同名称/签名和@VersionRange的方法,并调用它传递所有参数。

@RequestMapping批注支持一个headers元素,它允许您缩小匹配请求的范围。 特别是你可以在这里使用Accept头。

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

这不正是你所描述的,因为它不直接处理范围,但是元素确实支持*通配符和!=。 所以至less你可以避免在所有版本都支持有问题的端点的情况下使用通配符,或者甚至给定主要版本的所有次要版本(例如1. *)。

我不认为我以前实际上使用过这个元素(如果我不记得的话),所以我只是去掉文档

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

我已经实现了一个解决scheme,可以完全处理剩下的版本问题。

一般说来rest版本有三种主要的方法:

  • 基于path的方式,客户在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 
  • 自定义标题 ,客户端在自定义标题中定义版本。

一种方法的问题是,如果你改变版本让我们从v1 – > v2说,可能你需要复制粘贴没有改变到v2path的v1资源

第二种方法的问题是像http://swagger.io/这样的工具不能区分具有相同path但不同内容types的操作(检查问题https://github.com/OAI/OpenAPI-Specification/issues/ 146 )

解决scheme

由于我工作了很多rest文档工具,我更喜欢使用第一种方法。 我的解决scheme使用第一种方法处理问题 ,因此您不需要将端点复制粘贴到新版本。

假设我们有用户控制器的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"; } } 

要求是如果我请求v1的用户资源,我必须采取“用户V1” repsonse,否则,如果我请求的V2V3等,我必须采取“用户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 ; } 

}

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

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

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

比方说,我们改变了公司“合同”,打破了客户。 所以我们实现了http://localhost:9001/api/v2/company ,我们要求客户端改为v2而不是v1。

所以客户的新要求是:

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

代替:

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

这里最好的部分是通过这个解决scheme,客户端将从v2获取用户信息,从v2 获取公司信息, 而无需从用户v2创build新的(相同的)端点!

Rest文档正如我之前所说的,我select基于URL的版本控制方法的原因是,一些像Swagger这样的工具不能以不同的方式logging具有相同URL但不同内容types的端点。 使用此解决scheme,由于具有不同的URL,因此显示两个端点:

在这里输入图像描述

GIT

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

在生产中你可以有否定。 所以对于方法1说produces="!...1.7"并在方法2有正面的。

生产也是一个数组,所以你可以说方法1 produces={"...1.6","!...1.7","...1.8"}等(接受除1.7以外的所有)

当然这并不像你想到的范围那么理想,但是如果这是你系统中罕见的东西,我认为比其他定制的东西更容易维护。 祝你好运!

那么只使用inheritance来build模版本呢? 这就是我在我的项目中使用,它不需要特殊的弹簧configuration,并得到我正是我想要的。

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

这种设置允许代码的重复性很小,并且可以在很less的工作中将方法覆盖到新版本的API中。 这也节省了使用版本切换逻辑来复杂化源代码的需求。 如果你没有在一个版本中编码端点,它将默认抓取以前的版本。

相比别人正在做的事情,这似乎更容易。 有什么我失踪?

你可以使用AOP,截取左右

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

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

 @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 } 

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

对于AOPconfiguration,请看http://www.mkyong.com/spring/spring-aop-examples-advice/