I have implemented a solution which can handle the problem of rest versioning
perfectly.
Generally speaking, there are three major approaches to rest versioning.
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-Type header, in which the client defines the version in Accept header:
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.
第一种方法的问题在于,如果你从v1更改到v2版本,可能需要将未更改的v1资源复制粘贴到v2路径中。
第二种方法的问题在于,一些工具(例如http://swagger.io/)无法区分具有相同路径但不同Content-Type的操作(请查看https://github.com/OAI/OpenAPI-Specification/issues/146)。
解决方案
由于我经常使用rest文档工具,我更喜欢使用第一种方法。我的解决方案解决了第一种方法的问题,因此您无需将端点复制粘贴到新版本中。
假设我们为用户控制器有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;
@Controller
@Api(value = "user", description = "Operations about users")
public class UserController {
@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";
}
@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版本,则必须获取“User V1”响应;否则,如果我请求v2、v3等版本,则必须获取“User 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
,并要求客户端改为使用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
这里最好的部分是,借助这个解决方案,客户端将能够从v1获得用户信息,从v2获得公司信息,无需为v2用户创建一个新的(相同的)端点!
REST文档
正如我之前所说,我选择基于URL的版本控制方法的原因是,一些工具(如swagger)不会以不同的方式记录具有相同URL但不同内容类型的端点。使用这个解决方案,由于具有不同的URL,两个端点都会被显示:
GIT
解决方案实现在:
https://github.com/mspapant/restVersioningExample/