为什么要对API进行版本管理
因为软件有一个不断升级迭代的过程,而在升级中我们业务需求可能不断在更新,所以我们需要对以前的API接口不断的更新以满足变化的需求,但是在更新升级的过程中我们势必又要保证原来功能的可用性,所以我们就必须要对同一个API接口进行多版本的管理。
常用的API管理方式
1、不对API进行版本管理
每次版本升级都强制用户更新升级,这种方式非每次用户打开APP都要更新,非常影响用户的体验。然后这种方式会在旧系统过渡到新系统过程中让旧系统无法使用。并且这也会与业务冲突,很多场景业务就有需要新老系统并行的要求。
2、不同版本用不同域名
针对域名转发到部署了不同版本应用的服务器。如:http://v1.com ,v2.com,http://v3.com 的域名分别转发到部署了对应版本的应用服务器。
这种方式只有在系统比较稳定的时候,产品初期版本更新迭代比较快则不太适合,而且如果是每个版本都单独部署的话维护比较麻烦也比较浪费资源。
3、维护多个版本的代码(推荐)
首先在代码里面维护多个版本的API几口,然后在请求的URL、或者请求header里面带上一个版本参数,然后我们根据这个版本参数来区分调用对应版本的代码。
使用URL区分版本:
这种方式通常在代码里面我们会保存对应版本的controller方法,根据不同版本的URL 来分别调用对应的方法 如:
http://localhost:8888/test/v1/hello 、 http://localhost:8888/test/v2/hello 、 http://localhost:8888/test/v3/hello
分别调用我们的后台对应controller的方法
@RequestMapping("/test")
@RestController
public class TestAPiVersionController {
@RequestMapping("v1/hello")
public String hello(){
return "version 1";
}
@RequestMapping("v2/hello")
public String hello2(){
return "version 2";
}
@RequestMapping("v3/hello")
public String hello5(){
return "version 3";
}
}
通过保存多个版本的接口,然后在URL标识对应的版本号来调用对应的接口,这种方式解决了多版本管理的问题,但使用这种方式对于前端工程师来说也比较麻烦,必须在每个请求的URL上标记一个版本号来使用对应的版本的API,看着眼花缭乱的URL显然不太优雅。
所以我们会对这种方式进行一下优化。
优化目标
不需要在URL上标记版本号,前端只需要传一个当前APP的版本号,后端根据前端传过来的版本号,自动匹配到对应的版本API接口。
比如:
http://localhost:8888/test/hello hader 参数 apiversion 值 为v1 那么请求转发到v1版本的接口
http://localhost:8888/test/hello hader 参数 apiversion 值 为v2 那么请求转发到v2版本的接口
思路:
1、通过注解来标识不同接口的版本。
2、通过URL和请求Header 里面的apiversion值来建立与Controller不同版本方法的关系。
3、当一个请求进来,我们可以根据URL+apiversion 来执行对应版本的Controller方法。
如果不用@RequestMapping 的URL来标记不API的版本,那么势必我们得换一个方式来区分不同版本的API接口,这里我们用注解来实现。
/**
* 定义接口版本号
*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface ApiVersion {
int value();
}
Controller里面使用注解标识接口的版本号
@RequestMapping("/test")
@RestController
public class TestAPiVersionController {
@RequestMapping("hello")
@ApiVersion(1)
public Object hello(){
return "version 1";
}
@RequestMapping("hello")
@ApiVersion(2)
public Object hello2(){
return "version 2";
}
@RequestMapping("hello")
@ApiVersion(5)
public Object hello5(){
return "version 5";
}
}
重新建立URL+apiversion 与Controller方法之之间的关系需要扩展一下我们的SpringMVC的两个类RequestHandlerMapping,RequestCondition来实现。其实现大致逻辑如下图:
实现代码:
自定义ApiRequestCondition 实现RequestCondition
public class ApiRequestCondition implements RequestCondition<ApiRequestCondition> {
private int apiVersion;
public ApiRequestCondition(int apiVersion) {
this.apiVersion = apiVersion;
}
public int getApiVersion() {
return apiVersion;
}
@Override
public ApiRequestCondition combine(ApiRequestCondition other) {
// 采用最后定义优先原则,则方法上的定义覆盖类上面的定义
return new ApiRequestCondition(other.getApiVersion());
}
@Override
public int compareTo(ApiRequestCondition other, HttpServletRequest request) {
//对符合请求版本的版本号进行排序
return other.getApiVersion() - this.apiVersion;
}
@Override
public ApiRequestCondition getMatchingCondition(HttpServletRequest request) {
//设置默认版本号,请求版本号错误时使用最新版本号的接口
Integer version=10000;
//得到请求版本号
String apiversion=request.getHeader("apiversion");
if(StringUtils.isNotEmpty(apiversion)){
Matcher m = Pattern.compile("v(d+)").matcher(apiversion);
if (m.find()) {
version = Integer.valueOf(m.group(1));
}
}
// 返回小于等于请求版本号的版本
if (version >= this.apiVersion){
return this;
}
return null;
}
}
重写RequestMappingHandlerMapping
public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
@Override
protected RequestCondition<ApiRequestCondition> getCustomTypeCondition(Class<?> handlerType) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
return createCondition(apiVersion);
}
@Override
protected RequestCondition<ApiRequestCondition> getCustomMethodCondition(Method method) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
return createCondition(apiVersion);
}
private RequestCondition<ApiRequestCondition> createCondition(ApiVersion apiVersion) {
return apiVersion == null ? null : new ApiRequestCondition(apiVersion.value());
}
}
配置MVC使用自定义的CustomRequestMappingHandlerMapping (因为重写RequestMappingHandlerMapping 后会影响swagger的使用,所以在这里配置上swagger的资源路径映射)
@EnableSwagger2
@Configuration
public class WebApiVesionConfig extends WebMvcConfigurationSupport{
@Override
protected void addInterceptors(InterceptorRegistry registry) {
super.addInterceptors(registry);
}
@Override
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping();
handlerMapping.setOrder(0);
handlerMapping.setInterceptors(getInterceptors());
return handlerMapping;
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
super.addResourceHandlers(registry);
}
/**
* 配置servlet处理
*/
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
测试结果无误: