自定义RequestMappingHandlerMapping实现API版本控制
一、问题起源
对于接口版本控制问题,最开始我是采用的@RequestMapping(headers=“Api-Version=0.1”)的方式进行控制接口版本的,但是发现这样做会有一个问题:
假设现在的项目最新版本号为V0.3;
0.1版项目中有如下接口:
A(headers=“Api-Version=0.1”),B(headers=“Api-Version=0.1”);
后来版本升级到0.2版,0.2版里有0.1版已有的接口,另外又新增一个接口C,所以当前项目中有如下接口:
A(headers=“Api-Version=0.1”),B(headers=“Api-Version=0.1”),C(headers=“Api-Version=0.2”);
再后来又升级到0.3版,该版本同样拥有前两版已有的接口,同时,因需求变动,改动原有B接口(为了在后台更新版本时,不影响现有APP低版本用户,一般情况接口改动需要增加新接口),增加新接口B(headers=“Api-Version=0.3”);另外又增加接口D(headers=“Api-Version=0.3”),所以当前最新的项目中有如下接口:
A(headers=“Api-Version=0.1”),B(headers=“Api-Version=0.1”),C(headers=“Api-Version=0.2”), B(headers=“Api-Version=0.3”),D(headers=“Api-Version=0.3”);
那么,如果采用上述方案,按照@RequestMapping(headers=“Api-Version=XXX”)的方式来进行控制接口访问,调用不同的接口需要传所对应的不同的版本号,也就是在每个调用接口的地方,都需要单独的设置请求头参数,这样做就很麻烦了。
所以就在此处做了优化,采取扩展spring配置的方式,自定义RequestMappingHandlerMapping,从而实现:传版本号N,会自动映射到版本号小于等于N的接口中版本号最新的一个接口;
即APP调用时,只用在每个版本的APP的公共请求头里面加上对应版本号就可以由后端程序进行控制找到合适的接口并进行相应任务处理了。APP不用在调用接口处去考虑访问哪个版本的接口,直接交给后端程序来处理。
例如按照上述例子:
APP定义公共的请求头为Api-Version=0.3,即可访问到以下四个接口:
A(headers=“Api-Version=0.1”),C(headers=“Api-Version=0.2”),B(headers=“Api-Version=0.3”),D(headers=“Api-Version=0.3”);
而现在用户APP版本仍然停留在Api-Version=0.1未更新,当他们操作部分功能的时候,请求后端接口的请求头依然是Api-Version=0.1,这时,可访问的接口为:
A(headers=“Api-Version=0.1”),B(headers=“Api-Version=0.1”);
所以可以看出来,通过自定义RequestMappingHandlerMapping的这种方式可以很友好、灵活的解决API版本控制问题,从而提升整体开发的效率。
二、案例说明
1、requestMapping自定义规则配置
package im.web.version;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
/**
* 版本号规则配置类
* @author dawn
* Date:2019年4月12日
*/
public class ApiVesrsionCondition implements RequestCondition<ApiVesrsionCondition> {
private int apiVersion;
public ApiVesrsionCondition(int apiVersion){
this.apiVersion = apiVersion;
}
public ApiVesrsionCondition combine(ApiVesrsionCondition other) {
// 采用最后定义优先原则,则方法上的定义覆盖类上面的定义
return new ApiVesrsionCondition(other.getApiVersion());
}
public ApiVesrsionCondition getMatchingCondition(HttpServletRequest request) {
String ver = request.getHeader("Api-Version");
//因为请求头里面传来的是小数,所以需要乘以10
int version = (int) (Double.valueOf(ver) * 10);
if(version >= this.apiVersion) // 如果请求的版本号大于等于配置版本号, 则满足
return this;
return null;
}
public int compareTo(ApiVesrsionCondition other, HttpServletRequest request) {
// 优先匹配最新的版本号
return other.getApiVersion() - this.apiVersion;
}
public int getApiVersion() {
return apiVersion;
}
}
2、自定义RequestMappingHandlerMapping
package im.web.version;
import java.lang.reflect.Method;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
* 创建自定义requestMapping类来配置规则
* @author dawn
* Date:2019年4月12日
*/
public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
@Override
protected RequestCondition<ApiVesrsionCondition> getCustomTypeCondition(Class<?> handlerType) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
return createCondition(apiVersion);
}
@Override
protected RequestCondition<ApiVesrsionCondition> getCustomMethodCondition(Method method) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
return createCondition(apiVersion);
}
private RequestCondition<ApiVesrsionCondition> createCondition(ApiVersion apiVersion) {
return apiVersion == null ? null : new ApiVesrsionCondition(apiVersion.value());
}
}
3、继承WebMvcRegistrations接口实现getRequestMappingHandlerMapping方法,应用自定义的RequestMappingHandlerMapping
package im.web.version;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
* 覆盖spring原生RequestMappingHandlerMapping类
* @author dawn
* Date:2019年4月12日
*/
@Component
public class WebRequestMappingConfig implements WebMvcRegistrations {
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping();
handlerMapping.setOrder(0);
return handlerMapping;
}
}
4、增加版本管理注解
package im.web.version;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;
import org.springframework.web.bind.annotation.Mapping;
/**
* 控制api版本注解
* @author dawn
* Date:2019年4月12日
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
int value();
}
5、应用注解到控制器
@ApiVersion(1)
@RequestMapping("/test")
public void test1() {
System.out.println("method---test1");
}
@ApiVersion(2)
@RequestMapping("/test")
public void test2() {
System.out.println("method---test2");
}
6、通过HTTP请求/test接口,分别带上请求头Api-Version=0.1、Api-Version=0.2、Api-Version=0.8测试,可以看到控制台分别输出:
Api-Version=0.1:method—test1
Api-Version=0.2:method—test2
Api-Version=0.8:method—test2
ok,完成啦!
三、问题记录
配置过程中遇到了几个问题在此处记录一下:
1、继承WebMvcConfigurationSupport方式
使用的继承WebMvcConfigurationSupport来重写requestMappingHandlerMapping方法,代码如下,有部分省略:
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping();
handlerMapping.setOrder(0);
return handlerMapping;
}
}
这样做接口版本控制确实实现了,但是当我访问页面的时候就有问题了,发现我的js、css等静态资源都没有被加载,然后折腾了好一会儿,才发现spring的自动配置类WebMvcAutoConfiguration里面有如下注解:
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class):当前上下文中不存在WebMvcConfigurationSupport类型的Bean时才会加载当前配置。
所以,此处继承WebMvcConfigurationSupport导致spring的自动配置失效。
2、声明@EnableWebMvc注解方式
声明@EnableWebMvc注解,发现出现与上述相同问题,然后看了下注解定义类发现如下图所示:
@Import(DelegatingWebMvcConfiguration.class):导入DelegatingWebMvcConfiguration配置。
我们进入DelegatingWebMvcConfiguration类里面看看发现:
还是继承了WebMvcConfigurationSupport…
所以才会出现跟问题1一样的情况。
3、继承WebMvcConfigurerAdapter方式
看到有部分网友说,可以通过继承WebMvcConfigurerAdapter来实现我们想要的功能,但是我用的是springboot2.1.1版本,该版本的WebMvcConfigurerAdapter类已经被弃用。