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

在前一篇博文 springMVC修改接口注册映射逻辑 中,用修改springMVC注册和映射接口的逻辑实现了该功能,但缺点也很明显,客户端传入的版本号必须在服务端存在,如传入v2.0,则服务端必须存在v2.0版本的接口,否则会抛出404,无法实现降级调用最新接口或默认接口的功能。

网上有两篇博文 SpringMVC源码解读 - RequestMapping注解实现解读 - RequestCondition体系详解SpringMVC请求的时候是如何找到正确的Controller详细讲解了RequestConditionRequestMappingHandlerMapping真正实现接口匹配的是RequestCondition

RequestMappingInfo类是Spring3.1版本之后引入的。是一个封装了各种请求映射条件并实现了RequestCondition接口的类。有各种RequestCondition实现类属性,patternsConditionmethodsConditionparamsConditionheadersConditionconsumesCondition以及producesCondition,分别代表http请求的路径模式、方法、参数、头部等信息。

RequestMappingHandlerMapping的父类RequestMappingHandlerMapping中有两个方法getCustomTypeConditiongetCustomMethodCondition,这两个方法返回一个RequestCondition实例,默认实现返回null。在springMVC扫描注解创建映射时,获取的RequestCondition实例保存在RequestMappingInfocustomConditionHolder属性中。实现接口版本管理的关键是重写这两个方法,创建一个自定义的RequestCondition并实现比较。

首先创建一个注解ApiVersion,给方法标注版本号

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

创建自定义类ApiVersionCondition,实现RequestCondition接口

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
65
66
67
68
69
70
71
72
73
74
75
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition>{
private String version = "";//v1.0
public ApiVersionCondition(){
}
public ApiVersionCondition(String version){
if(null!=version){
this.version = version.toLowerCase();
}
}
public String getVersion(){
return this.version;
}
@Override
public ApiVersionCondition combine(ApiVersionCondition other) {
return new ApiVersionCondition(other.getVersion());
}
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
String requestVersion = NormRequestUtil.getRequestVersion(request);
if(compareVersion(requestVersion,this.version)>=0){
return this;
}
return null;
}
@Override
public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
return compareVersion(other.getVersion(),this.version);
}
private int compareVersion(String version1,String version2){
try{
String pattern = Pattern.compile("[a-z\\s]").pattern();
String newVersion1 = version1.replaceAll(pattern,"");
String newVersion2 = version2.replaceAll(pattern,"");
String[] v1 = newVersion1.split(".");
String[] v2 = newVersion2.split(".");
if(v1.length>v2.length){
v2 = fillArrayByZero(v2,v1.length);
}
if(v2.length>v1.length){
v1 = fillArrayByZero(v1,v2.length);
}
for(int i=0;i<v1.length;i++){
int ver1 = NumberUtils.toInt(v1[i]);
int ver2 = NumberUtils.toInt(v2[i]);
if(ver1!=ver2){
return ver1 - ver2;
}
}
DICLogger.info("用数字方式逐号比较版本结果一致,尝试重新使用字符比较");
return version1.compareTo(version2);
}catch (Exception e){
DICLogger.error("将版本转换成数字比较时发生错误,降级用字符串比较,可能无法精准匹配到指定版本接口,请检查版本号",e);
}
return version1.compareTo(version2);
}
private String[] fillArrayByZero(String[] arr,int totalLength){
if(arr.length>totalLength){
return arr;
}
String[] newV = new String[totalLength];
for(int i=0;i<newV.length;i++){
if(i<arr.length){
newV[i] = arr[i];
}else{
newV[i] = "0";
}
}
return newV;
}
}

实现比较简单,重要的是getMatchingCondition方法,在上面实现中,所有比请求版本号小的接口都是符合条件的,compareTo方法比对所有符合条件的接口,取最新的接口。

重写getCustomTypeConditiongetCustomMethodCondition方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CpctRequestMappingHandlerMapping extends RequestMappingHandlerMapping{
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
ApiVersion version = AnnotationUtils.findAnnotation(handlerType,ApiVersion.class);
return createCondition(version);
}
protected RequestCondition<?> getCustomMethodCondition(Method method) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
return createCondition(apiVersion);
}
private RequestCondition<ApiVersionCondition> createCondition(ApiVersion apiVersion) {
return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value());
}
}

修改applicationContext-servlet.xml

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>

示例

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