一、背景与思路
其实标题中想要干的事,就是让你的工程同时支持多个版本的request请求
不说怎么玩儿,先说说这么玩儿的必要性把。我感觉真的没啥卵用。
因为这么做的一个大前提,一定是你的项目的持续时间会很长。同时在这个期间内,各种终端(比如手机)的硬件版本会同时存在多个版本的情况,我们既要让老用户开开心心的继续使用老版本,还要让新用户痛痛快快的获得最新体验,标题中的需求才有意义。否则,这个需求真的就是个蛋。直接让用户升级版本就好了。
既然总有刁民想害朕,那就关闭吐槽模式,下面就说怎么玩儿吧。
第一 大前提是:重复的垃圾代码尽量少
核心指导思想就是“最近最高原則”,实现向上兼容。
第二,对上面所说的分别加以解释
2.1:在设计之初,就要考虑到未来的多版本并存的问题。那么就要在rest请求的路径中预留出版本的位置。
2.2:并非所有的代码都会在每个版本有升级。那么老的代码一定要能支撑高版本的请求才合理。
举例子,比如有一段代码,我们实现了v1.0,v1.2,v3.1这三个版本,那么当有一个版本是v2.0的请求过来
根据“最近最高原则”,它应该自动匹配到我们的v1.2版本才对。而且这样也才满足我们希望重复代码尽量少的愿望
二、实现方法
先盗个图
由上图可知,其实每个request请求最终都要落实到某个具体干活儿的controller的某个方法上面。
而“落实”或者叫“映射”的这一步,就是有一个叫“HandlerMapping”的springMVC的核心组件干的。
其实现类叫做“RequestMappingHandlerMapping”
所以,如果想要实现题目中的需求,解决的核心就要重写“RequestMappingHandlerMapping”这个类。
随手配个图,反正我是不看它的
HandlerMapping就是通过继承InitializingBean接口在完成实例后,扫描所有的Controller类和标有RequestMapping的方法。然后在应用运行的时候,根据请求的request来找到相应的handler来处理这个请求。
三、开干
1,创建一个注解,后续添加到controller或者controller中的方法
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
/**
* 标识版本号
* @return
*/
String value();
}
“@Retention”注解
说明我们定义的这个ApiVersion是个注解
同时这个自定义注解的生存周期是“RetentionPolicy.RUNTIME”,即生存于整个运行时始终的
“@Target”注解
说明挡墙注解的作用范围
“ElementType.METHOD,ElementType.TYPE”表示范围包括“方法”和“类、接口”
2,让这个注解起作用
上面我们说过了,需要重写“RequestMappingHandlerMapping”,下面就是重写的例子
public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
@Override
protected RequestCondition<ApiVersionCondition> getCustomTypeCondition(Class<?> handlerType) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
return createCondition(apiVersion);
}
@Override
protected RequestCondition<ApiVersionCondition> 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());
}
}
上面这段代码会被RequestMappingHandlerMapping干活儿时自动调用对应方法。
与这个类相关的类还有下面的这个
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
// 版本的格式,這里用的是形如“1_0_6”的版本号
private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile("\\d_\\d_\\d");
// 配置在配置文件中的版本列表,如右侧所示 version.list=1_0_0,1_0_2,1_0_6
// 其作用是向上兼容,如果请求中的版本号是当前系统没有的,那么就需要从这个列表中寻找所有低于请求版本号的最高的一个
private static final List<String> vl = Arrays.asList(PropertiesUtils.getStringByKey("version.list").split(","));
/**
* api的版本
*/
private String apiVersion;
public ApiVersionCondition(String apiVersion) {
this.apiVersion = apiVersion;
}
/**
* 将不同的筛选条件合并
*
* @param apiVersionCondition
* @return
*/
@Override
public ApiVersionCondition combine(ApiVersionCondition apiVersionCondition) {
// 假如你的controller层代码中,会对类与方法都加上了关于版本号的定义
// 那么采用最后定义优先原则,则方法上的定义覆盖类上面的定义
return new ApiVersionCondition(apiVersionCondition.getApiVersion());
}
/**
* 根据request查找匹配到的筛选条件
*
* @param httpServletRequest
* @return
*/
@Nullable
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {
System.out.println(httpServletRequest.getRequestURI());
Matcher m = VERSION_PREFIX_PATTERN.matcher(httpServletRequest.getRequestURI());
if (m.find()) {
String urlVersion = m.group(0);
if (vl.contains(urlVersion)) {
if (vl.indexOf(urlVersion) >= vl.indexOf(this.apiVersion)) {
return this;
}
}
}
return null;
}
/**
* 其作用见vl变量的说明部分
*
* @param apiVersionCondition
* @param httpServletRequest
* @return
*/
@Override
public int compareTo(ApiVersionCondition apiVersionCondition, HttpServletRequest httpServletRequest) {
// return 0;
// 优先匹配最新的版本号
return vl.indexOf(apiVersionCondition.getApiVersion()) - vl.indexOf(this.apiVersion);
}
public String getApiVersion() {
return apiVersion;
}
}
最后,得让SpringMVC加载我们定义的CustomRequestMappingHandlerMapping以覆盖原先的RequestMappingHandlerMapping。才能让上面的这些代码真正跑起来,如下所示
@Configuration
public class WebConfig extends WebMvcConfigurationSupport{
@Override
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping();
handlerMapping.setOrder(0);
handlerMapping.setInterceptors(getInterceptors());
return handlerMapping;
}
}
3,写个controller看看吧
@ApiVersion("1_0_1")
@RestController
@RequestMapping("/pay/{version}")
public class BasePayOrderController1_0_1 {
@RequestMapping(value="/createOrder",method=RequestMethod.POST)
@AuthCheckAnnotation
public Object createOrder(PayOrderVO payOrderVO) {
System.out.println("createOrder 1.0.1");
return null;
}
}
@ApiVersion("1_0_2")
@RestController
@RequestMapping("/pay/{version}")
public class BasePayOrderController1_0_2 {
@RequestMapping(value="/createOrder",method=RequestMethod.POST)
@AuthCheckAnnotation
public Object createOrder(PayOrderVO payOrderVO) {
System.out.println("createOrder 1.0.2");
return null;
}
}
上面是将同一个业务根据不同的版本定义了不同controller的类。这种方法比较笨。
另一种是,所有的相同业务的都放在同一个controller类中,然后在方法上面按照版本来区分
@RestController
@RequestMapping("/pay/{version}")
public class BasePayOrderController1_0_1 {
@ApiVersion("1_0_1")
@RequestMapping(value="/createOrder",method=RequestMethod.POST)
@AuthCheckAnnotation
public Object createOrder(PayOrderVO payOrderVO) {
System.out.println("createOrder 1.0.1");
return null;
}
@ApiVersion("1_0_2")
@RequestMapping(value="/createOrder",method=RequestMethod.POST)
@AuthCheckAnnotation
public Object createOrder(PayOrderVO payOrderVO) {
System.out.println("createOrder 1.0.2");
return null;
}
}
这样的效果与上面是相同的。
到此告一段落。