[英]How to manage REST API versioning with spring?
我一直在寻找如何使用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;
}
这在春天是不可能的,因为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;
}
通过这种方式,我可以在注释的产生部分中定义关闭或打开的版本范围。 我现在正在研究这个解决方案,我仍然需要更换一些我不喜欢的核心Spring MVC类( RequestMappingInfoHandlerMapping
, RequestMappingHandlerMapping
和RequestMappingInfo
),因为这意味着每当我决定升级到更新版的春天。
我会很感激任何想法......特别是,任何建议都可以用更简单,更容易维护的方式来做到这一点。
添加赏金。 为了得到赏金,请回答上面的问题,而不建议在控制器本身有这个逻辑。 Spring已经有很多逻辑来选择调用哪个控制器方法,我想捎带它。
我在github中分享了原始的POC(有一些改进): https : //github.com/augusto/restVersioning
无论是否可以通过向后兼容的更改来避免版本控制(当您受到某些公司指南的约束时可能并不总是可行,或者您的API客户端以错误的方式实现,并且即使它们不应该会破坏),抽象的需求也是一个有趣的一:
如何在不在方法体中进行评估的情况下执行自定义请求映射,从而对请求中的标头值进行任意评估?
如本SO答案中所述,您实际上可以使用相同的@RequestMapping
并使用不同的注释来区分在运行时期间发生的实际路由。 为此,您必须:
VersionRange
。 RequestCondition<VersionRange>
。 由于您将拥有类似最佳匹配算法的内容,因此您必须检查使用其他VersionRange
值注释的方法是否为当前请求提供了更好的匹配。 VersionRangeRequestMappingHandlerMapping
(如帖子如何实现@RequestMapping自定义属性中所述 )。 RequestMappingHandlerMapping
(例如,通过将其顺序设置为0)之前,配置spring以评估VersionRangeRequestMappingHandlerMapping
。 这不需要任何Spring组件的hacky替换,但使用Spring配置和扩展机制,因此即使您更新Spring版本它也应该工作(只要新版本支持这些机制)。
我刚创建了一个自定义解决方案 我在@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");
}
}
我仍然建议使用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;
}
}
基于最后一种方法,您实际上可以实现您想要的东西。
例如,您可以拥有一个仅包含版本处理方法的控制器。
在该处理中,您可以在某些弹簧服务/组件中查找(使用反射/ AOP /代码生成库),或者在同一类中查找具有相同名称/签名且需要@VersionRange的方法,并调用它传递所有参数。
我已经实现了一个解决方案,可以完美地处理休息版本控制的问题。
一般来说,有三种主要的休息版本方法:
基于路径的approch,客户端在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,可能需要将未更改的v1资源复制粘贴到v2路径
第二种方法的问题是像http://swagger.io/这样的工具不能区分具有相同路径但内容类型不同的操作(检查问题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";
}
}
要求是如果我请求用户资源的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 ;
}
}
该实现读取URL中的版本并从spring请求解析URL。如果此URL不存在(例如客户端请求v3 ),那么我们尝试使用v2 ,直到我们找到该资源的最新版本 。
为了看到这种实现的好处,假设我们有两个资源:用户和公司:
http://localhost:9001/api/v{version}/user
http://localhost:9001/api/v{version}/company
假设我们改变了打破客户的公司“合同”。 所以我们实现了http://localhost:9001/api/v2/company
,我们要求客户端在v1上改为v2。
所以客户的新请求是:
http://localhost:9001/api/v2/user
http://localhost:9001/api/v2/company
代替:
http://localhost:9001/api/v1/user
http://localhost:9001/api/v1/company
这里最好的部分是,使用此解决方案,客户端将从v1获取用户信息,从v2 获取公司信息, 而无需从用户v2创建新的(相同)端点!
休息文档正如我之前所说的,我选择基于URL的版本控制方法的原因是,像swagger这样的工具不会以不同的方式记录具有相同URL但内容类型不同的端点。 使用此解决方案,由于具有不同的URL,因此显示两个端点:
GIT
解决方案实施: https : //github.com/mspapant/restVersioningExample/
@RequestMapping
注释支持headers
元素,允许您缩小匹配请求。 特别是,您可以在此处使用Accept
标头。
@RequestMapping(headers = {
"Accept=application/vnd.company.app-1.0+json",
"Accept=application/vnd.company.app-1.1+json"
})
这并不是您所描述的,因为它不直接处理范围,但该元素确实支持*通配符以及!=。 因此,至少你可以使用通配符来解决所有版本都支持相关端点的情况,甚至是给定主要版本的所有次要版本(例如1. *)。
我不认为我之前实际使用过这个元素(如果我不记得的话),所以我只是在文档中删除
如何使用继承来建模版本? 这就是我在我的项目中使用的东西,它不需要特殊的弹簧配置,让我得到我想要的东西。
@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);
}
}
这种设置允许很少的代码重复,并且能够用很少的工作将方法覆盖到新版本的api中。 它还节省了使用版本切换逻辑使源代码复杂化的需要。 如果您没有在版本中编写端点,它将默认获取以前的版本。
与其他人相比,这似乎更容易。 有什么我想念的吗?
在产品中你可以有否定。 因此对于method1来说,produce produces="!...1.7"
并且在method2中有正数。
产品也是一个数组,所以你对method1你可以说produces={"...1.6","!...1.7","...1.8"}
等(接受除1.7之外的所有)
当然,并不像你想到的那样理想,但我觉得比其他自定义的东西更容易维护,如果这在你的系统中是不常见的。 祝好运!
我已经尝试使用URI Versioning对我的API进行版本控制 ,例如:
/api/v1/orders
/api/v2/orders
但是在尝试实现这项工作时存在一些挑战:如何使用不同的版本组织代码? 如何同时管理两个(或更多)版本? 删除某些版本有什么影响?
我发现的最佳替代方案不是整个API 的版本 ,而是控制每个端点上的版本 。 此模式称为使用Accept标头进行 版本控制或通过内容协商进行版本控制 :
这种方法允许我们对单个资源表示进行版本控制,而不是对整个API进行版本控制,这使我们能够更精细地控制版本控制。 它还在代码库中创建了更小的占用空间,因为我们在创建新版本时不必分叉整个应用程序。 这种方法的另一个优点是它不需要实现通过URI路径进行版本控制引入的URI路由规则。
首先,创建一个具有基本生成属性的Controller,默认情况下将为类中的每个端点应用该属性。
@RestController
@RequestMapping(value = "/api/orders/", produces = "application/vnd.company.etc.v1+json")
public class OrderController {
}
之后,创建一个可能的场景,其中有两个版本的端点用于创建订单:
@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);
}
完成! 只需使用所需的Http Header版本调用每个端点:
Content-Type: application/vnd.company.etc.v1+json
或者,调用版本二:
Content-Type: application/vnd.company.etc.v2+json
关于你的担忧:
并且由于并非API中的所有方法都在同一版本中发生更改,因此我不想转到每个控制器并更改任何版本之间未更改的处理程序
如上所述,此策略使用其实际版本维护每个Controller和端点。 您只需修改具有修改且需要新版本的端点。
使用此策略,使用不同版本设置Swagger也非常容易。 请参阅此答案以了解更多详情。
你可以在拦截周围使用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
}
唯一的限制是所有必须在同一个控制器中。
有关AOP配置, 请查看http://www.mkyong.com/spring/spring-aop-examples-advice/
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.