我一直在寻找如何使用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;
}
这在 Spring 天是不可能的,因为2个方法具有相同的 RequestMapping
注释并且Spring无法加载 . 这个想法是 VersionRange
注释可以定义一个开放或封闭的版本范围 . 第一种方法从版本1.0到1.6有效,而第二种方法从版本1.7开始(包括最新版本1.8) . 我知道如果有人决定通过99.99版本,这种方法会中断,但是's something I' m可以接受 .
现在,由于如果没有对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;
}
通过这种方式,我可以在注释的产生部分中定义关闭或打开的版本范围 . 我现在正在研究这个解决方案,问题是我仍然需要更换一些我不喜欢的核心Spring MVC类( RequestMappingInfoHandlerMapping
, RequestMappingHandlerMapping
和 RequestMappingInfo
),因为这意味着每当我决定升级到更新版的 Spring 天 .
我会很感激任何想法......特别是,任何建议都可以用更简单,更容易维护的方式来做到这一点 .
编辑
添加赏金 . 为了得到赏金,请回答上面的问题,而不建议在控制器本身中使用这个逻辑 . Spring已经有很多逻辑来选择调用哪个控制器方法,我想捎带它 .
编辑2
我在github中分享了原始的POC(有一些改进):https://github.com/augusto/restVersioning
8 回答
无论是否可以通过向后兼容的更改来避免版本控制(当您受到某些公司指南的约束时可能并不总是可行,或者您的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?
如this SO answer中所述,您实际上可以使用相同的
@RequestMapping
并使用不同的注释来区分在运行时期间发生的实际路由 . 为此,您必须:创建新注释
VersionRange
.实施
RequestCondition<VersionRange>
. 由于您将拥有类似最佳匹配算法的内容,因此您必须检查使用其他VersionRange
值注释的方法是否为当前请求提供了更好的匹配 .基于注释和请求条件实现
VersionRangeRequestMappingHandlerMapping
(如How to implement @RequestMapping custom properties中所述) .配置 spring 以在使用默认
RequestMappingHandlerMapping
之前评估VersionRangeRequestMappingHandlerMapping
(例如,通过将其顺序设置为0) .这不需要任何Spring组件的hacky替换,但使用Spring配置和扩展机制,因此即使您更新Spring版本它也应该工作(只要新版本支持这些机制) .
我刚创建了一个自定义解决方案我在
@Controller
类中使用了@ApiVersion
注释和@RequestMapping
注释 .示例:
实施:
ApiVersion.java 注释:
ApiVersionRequestMappingHandlerMapping.java (这主要是从
RequestMappingHandlerMapping
复制和粘贴):注入WebMvcConfigurationSupport:
我仍然建议使用URL进行版本控制,因为在URL中,@ RequestMapping支持模式和路径参数,可以使用regexp指定哪种格式 .
和要处理客户端升级(您在评论中提到),您可以使用“最新”之类的别名 . 或者使用最新版本的api的无版本版本(是的) .
同样使用路径参数,您可以实现任何复杂的版本处理逻辑,如果您已经想要有范围,那么您很可能想要更快的东西 .
以下是几个例子:
Based on the last approach you can actually implement something like what you want.
例如,您可以拥有一个仅包含版本处理方法的控制器 .
在该处理中,您可以在某些 spring 服务/组件中查找(使用反射/ AOP /代码生成库),或者在同一类中查找具有相同名称/签名且需要@VersionRange的方法,并调用它传递所有参数 .
我已经实现了一个处理 PERFECTLY 其余版本控制问题的解决方案 .
一般来说,有三种主要的休息版本方法:
基于
problem 与 first 方法是如果您更改版本让's say from v1 -> v2, probably you need to copy-paste the v1 resources that haven' t更改为v2路径
使用 second 方法的 problem 是某些工具(如http://swagger.io/)无法区分具有相同路径但内容类型不同的操作(检查问题https://github.com/OAI/OpenAPI-Specification/issues/146)
The solution
由于我正在使用其他文档工具,我更喜欢使用第一种方法 . 我的解决方案使用第一种方法处理 problem ,因此您无需将 endpoints 复制粘贴到新版本 .
假设我们有用户控制器的v1和v2版本:
requirement 如果我请求 v1 为用户资源我必须采取 "User V1" repsonse,否则如果我请求 v2 , v3 等等我必须采取 "User V2" 响应 .
为了在spring中实现这一点,我们需要覆盖默认的 RequestMappingHandlerMapping 行为:
}
该实现读取URL中的版本并从spring请求解析URL . 如果此URL不存在(例如客户端请求 v3 ),那么我们尝试使用 v2 ,直到我们找到资源的 most recent version .
为了看到这种实现的好处,假设我们有两个资源:用户和公司:
假设我们改变了公司"contract",打破了客户 . 所以我们实现
http://localhost:9001/api/v2/company
并且我们要求客户端改为v2而不是v1 .所以客户的新请求是:
代替:
这里的 best 部分是使用此解决方案,客户端将从v1获取用户信息,从v2 without the need 获取公司信息,以从用户v2创建新的(相同) endpoints !
Rest Documentation 正如我之前所说的,选择基于URL的版本控制方法的原因是,像swagger这样的工具不会以不同的方式记录具有相同URL但内容类型不同的 endpoints . 使用此解决方案,由于具有不同的URL,因此显示两个 endpoints :
GIT
解决方案实施位于:https://github.com/mspapant/restVersioningExample/
@RequestMapping
注释支持headers
元素,允许您缩小匹配请求 . 特别是你可以在这里使用Accept
Headers .这并不是您所描述的,因为它不直接处理范围,但该元素确实支持*通配符以及!= . 因此,至少你可以使用通配符来解决所有版本都支持相关 endpoints 的情况,甚至是给定主要版本的所有次要版本(例如1. *) .
我不认为我之前实际使用过这个元素(如果我不记得的话),所以我只是在文档中删除
http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestMapping.html
如何使用继承来建模版本?这就是我在我的项目中使用的东西,它不需要特殊的 spring 配置,让我得到我想要的东西 .
这种设置允许很少的代码重复,并且能够用很少的工作将方法覆盖到新版本的api中 . 它还节省了使用版本切换逻辑使源代码复杂化的需要 . 如果您没有在版本中编写 endpoints ,它将默认获取以前的版本 .
与其他人相比,这似乎更容易 . 我有什么东西吗?失踪?
在产品中你可以有否定 . 所以对于method1来说
produces="!...1.7"
并且在method2中有正面 .产品也是一个数组,所以你可以说method1,你可以说
produces={"...1.6","!...1.7","...1.8"}
等(接受除1.7以外的所有)当然,并不像你想到的那样理想,但我觉得比其他自定义的东西更容易维护,如果这在你的系统中是不常见的 . 祝好运!
你可以在拦截周围使用AOP
考虑有一个请求映射,它接收所有
/**/public_api/*
,并且在这个方法中什么都不做;后
唯一的限制是所有必须在同一个控制器中 .
对于AOP配置,请查看http://www.mkyong.com/spring/spring-aop-examples-advice/