如何令一个SpingBoot工程同时支持不同版本的Restful請求

一、背景与思路

其实标题中想要干的事,就是让你的工程同时支持多个版本的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;
	}
}

这样的效果与上面是相同的。

到此告一段落。

 

参考文献1

参考文献2

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值