最近的一个需求,在现有的系统上实现同一接口的多版本访问,便于以后的接口升级,客户端不改变请求地址,在参数中传递一个version字段指定访问哪个版本的接口。

因为现系统已经在运行,所以改造不能影响已在使用的接口,最开始的设想是建立一个新接口,然后用拦截器拦截请求,组装请求地址+版本号,重定向到新接口,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Controller
@RequestMapping("cpct")
public class TestController {
@ResponseBody
@RequestMapping("param")
public Object versionApi1(HttpServletRequest request){
return NormResultUtil.resultSuccess(request,"成功请求接口cpct/param,默认版本");
}
@ResponseBody
@RequestMapping("param/v20")
public Object versionApi2(HttpServletRequest request){
return NormResultUtil.resultSuccess(request,"成功请求接口cpct/param,版本v2.0");
}

像上面那样,建立一个cpct/param/v20的新接口,客户端请求cpct/param.action?version=v2.0时重定向成cpct/param/v20.action,想法是美好的,客户端ajax请求根本无法实现重定向。

就因为开始的想法是这样,后面就华丽丽地跑偏了,脑子里全想的是怎么在服务端实现接口重定向,走了一大截弯路(当然这种方式也是能实现多版本控制的)。

拦截重定向行不通,就试试从springMVC的RequestMappingHandlerMapping的接口注册和查询逻辑上入手,迂回实现上面的功能。在方法上加个注解标注版本

1
2
3
4
5
6
7
8
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
public static final String DEFAULT_VERSION = "v1.0";
String value() default DEFAULT_VERSION;
}

1
2
3
4
5
6
@ResponseBody
@RequestMapping("param")
@ApiVersion("v2.0")
public Object versionApi2(HttpServletRequest request){
return NormResultUtil.resultSuccess(request,"成功请求接口cpct/param,版本v2.0");
}

翻阅RequestMappingHandlerMapping源码,在扫描方法的RequestMapping注解时有一个很重要的方法getMappingForMethod,它将含有RequestMapping注解的方法信息获取到生成RequestMappingInfo对象,作为键放在一个Map中(这个Map是内部类MappingRegistry的一个属性mappingLookup)。

幸好getMappingForMethod方法是protected的,重写这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class CpctRequestMappingHandlerMapping extends RequestMappingHandlerMapping{
@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
//调用父方法获取映射信息
RequestMappingInfo info = super.getMappingForMethod(method,handlerType);
//获取接口版本注解信息
ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
//所有接口默认v1.0,不做处理,只处理不是v1.0的接口
if(info!=null&&apiVersion!=null&&!StringUtils.equals(apiVersion.value(),ApiVersion.DEFAULT_VERSION)){
String version = apiVersion.value().replaceAll("\\.","");
//获取映射模式
PatternsRequestCondition patternsRequestCondition = info.getPatternsCondition();
//获取映射地址集合,这个集合是不可修改的,集合值就是接口地址
Set<String> patternSet = patternsRequestCondition.getPatterns();
Iterator<String> itr =patternSet.iterator();
//建立一个新集合放拼装后的地址
Set<String> newSet = new HashSet<>();
//接口分隔默认"/"
String separator = AntPathMatcher.DEFAULT_PATH_SEPARATOR;
//获取java运行环境的实际分隔符
try{
Field field = patternsRequestCondition.getClass().getDeclaredField("pathMatcher");
field.setAccessible(true);
Object pathMatcher = field.get(patternsRequestCondition);
if(pathMatcher instanceof AntPathMatcher){
AntPathMatcher antPathMatcher = (AntPathMatcher) pathMatcher;
Field pathSeparatorField = antPathMatcher.getClass().getDeclaredField("pathSeparator");
pathSeparatorField.setAccessible(true);
Object pathSeparator = pathSeparatorField.get(antPathMatcher);
if(pathSeparator!=null&&StringUtils.isNotBlank((String)pathSeparator)){
separator = (String)pathSeparator;
}
}
}catch (Exception e){
//错误不处理,使用默认的分隔符
}
while (itr.hasNext()){
String pattern = itr.next();
newSet.add(pattern+separator+version);//拼装新地址
}
//设置新的地址集合
try {
newSet = Collections.unmodifiableSet(newSet);
Field field = patternsRequestCondition.getClass().getDeclaredField("patterns");
field.setAccessible(true);
field.set(patternsRequestCondition,newSet);
} catch (Exception e) {
DICLogger.error("根据接口版本重写接口地址失败",e);
}
//返回一个新的地址映射对象,避免springMVC抛出接口映射已经注册的错误
info = new RequestMappingInfo(
info.getName(),
info.getPatternsCondition(),
info.getMethodsCondition(),
info.getParamsCondition(),
info.getHeadersCondition(),
info.getConsumesCondition(),
info.getProducesCondition(),
info.getCustomCondition()
);
}
return info;
}
}

这样,所有带有@ApiVersion注解的接口都换成了新地址。在客户端请求时,需要修改获取接口的逻辑。RequestMappingHandlerMapping类从AbstractHandlerMethodMapping类继承了一个重要的方法getHandlerInternal,这个方法就是根据请求信息获取对应的地址。原方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
if (logger.isDebugEnabled()) {
logger.debug("Looking up handler method for path " + lookupPath);
}
this.mappingRegistry.acquireReadLock();
try {
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
if (logger.isDebugEnabled()) {
if (handlerMethod != null) {
logger.debug("Returning handler method [" + handlerMethod + "]");
}
else {
logger.debug("Did not find handler method for [" + lookupPath + "]");
}
}
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
this.mappingRegistry.releaseReadLock();
}
}

最开始尝试重写这个方法,但是方法内部做了线程同步而mappingRegistry这个属性私有无法访问。调试发现从request中获取请求信息是通过UrlPathHelper这个工具类的getLookupPathForRequest方法完成的。AbstractHandlerMethodMapping类有一个公共方法getUrlPathHelper获取UrlPathHelper实例。所以解决方式就是继承UrlPathHelper,重写getLookupPathForRequest方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CustomerUrlPathHelper extends UrlPathHelper{
public String getLookupPathForRequest(HttpServletRequest request) {
String path = super.getLookupPathForRequest(request);
String version = NormRequestUtil.getRequestVersion(request);
//同样不处理默认的v1.0版本
if(StringUtils.isNotBlank(version)&&!version.equals(ApiVersion.DEFAULT_VERSION)){
if(!version.startsWith("v")){
version = "v"+version;
}
version = version.replaceAll("\\.","");
path = path.replace(".action","/"+version+".action");
}
return path;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CpctRequestMappingHandlerMapping extends RequestMappingHandlerMapping{
private UrlPathHelper urlPathHelper = new CustomerUrlPathHelper();
public void setAlwaysUseFullPath(boolean alwaysUseFullPath) {
super.setAlwaysUseFullPath(alwaysUseFullPath);
this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath);
}
public void setUrlDecode(boolean urlDecode) {
super.setUrlDecode(urlDecode);
this.urlPathHelper.setUrlDecode(urlDecode);
}
public void setRemoveSemicolonContent(boolean removeSemicolonContent) {
super.setRemoveSemicolonContent(removeSemicolonContent);
this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent);
}
public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
super.setUrlPathHelper(new CustomerUrlPathHelper());
this.urlPathHelper = new CustomerUrlPathHelper();
}
public UrlPathHelper getUrlPathHelper() {
return this.urlPathHelper;
}
....
}

CpctRequestMappingHandlerMapping类中添加如上代码,为了以防万一,重写了所有给urlPathHelper设置属性的代码,同时调用父类的相关方法。

修改application-servlet.xml配置。如果有<mvc:annotation-driven>配置,需要注释掉,手动配置

1
2
3
4
5
6
7
8
<bean id="handlerMapping" class="com.xxx.core.apiversion.CpctRequestMappingHandlerMapping"/>
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="messageConverters">
<list>
<bean class="com.xxx.NormMappingJackson2HttpMessageConverter"/>
</list>
</property>
</bean>

在实际开发过程中这样做没有多少实际意义,springMVC本身扩展性比较强,大多数需求都可以通过扩展完成,但这也是一种思路,万一哪天遇到一些特殊的需求呢。

RequestMappingHandlerMapping源码解读可以参考:
SpringMVC源码解读 - HandlerMapping - RequestMappingHandlerMapping初始化
springMVC (十) RequestMappingHandlerMapping