1. 说明

随着业务不断的升级优化,会导致新老版本的api入参和出参不一致。在实际场景中又不能每次修改接口都要求客户端强制升级,这样会使用户体验变得很差。而多版本接口的设计就是为了兼容新老版本的差异,从而提高用户体验。

2. 多版本策略

管理策略 示例
域名区分 v1.api.com
v2.api.com
请求url path /v1/test/index
/v2/test/index
请求参数区分 /test/index?v=v1
/test/index?v=v2

3. 请求(url path)实现

基于SpringBoot(2.3.5.RELEASE)实现;

3.1 自定义版本注解

新建文件: src/main/java/com/hui/apiversion/annotion/ApiVersion.java

package com.hui.apiversion.annotion;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)

/**
* 版本控制器
*/
public @interface ApiVersion {
// 标识版本号
int value() default 1;
}

3.2 自定义匹配URL

新建文件: src/main/java/com/hui/apiversion/config/ApiVersionCondition.java

package com.hui.apiversion.config;

import lombok.Data;
import org.springframework.web.servlet.mvc.condition.RequestCondition;

import javax.servlet.http.HttpServletRequest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* 定义url匹配条件
*/
@Data
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
/**
* 匹配url路径中的版本,如/v[1-9]/
*/
private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+)");

// api的版本
private int apiVersion;

/**
* @param apiVersion
*/
public ApiVersionCondition(int apiVersion){
this.apiVersion = apiVersion;
}

/**
* 将不同的条件进行合并
* @param apiVersionCondition
* @return
*/
@Override
public ApiVersionCondition combine(ApiVersionCondition apiVersionCondition) {
// 以最后定义的版本为准,即最后版本的定义的方法覆盖上以前版本的方法
return new ApiVersionCondition(apiVersionCondition.getApiVersion());
}

/**
* 根据request查找匹配到的筛选条件
* @param httpServletRequest
* @return
*/
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {
Matcher matcher = VERSION_PREFIX_PATTERN.matcher(httpServletRequest.getRequestURI());
if (matcher.find()) {
int version = Integer.parseInt(matcher.group(1));
if (version >= this.apiVersion) {
return this;
}
}
return null;
}

/**
* 不同筛选条件比较,用于版本排序(最新的版本 > 老版本)
* @param apiVersionCondition
* @param httpServletRequest
* @return
*/
@Override
public int compareTo(ApiVersionCondition apiVersionCondition, HttpServletRequest httpServletRequest) {
return apiVersionCondition.getApiVersion() - this.apiVersion;
}
}

3.3 自定义匹配处理器

新建文件: src/main/java/com/hui/apiversion/config/ApiVersionConfig.java

package com.hui.apiversion.config;
import com.hui.apiversion.annotion.ApiVersion;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.resource.ResourceUrlProvider;

import java.lang.reflect.Method;

@Configuration
public class ApiVersionConfig extends WebMvcConfigurationSupport {
/**
* 重写请求过处理的方法
* 在使用@RequestMapping注解时,SpringMvc通过RequestMappingHandlerMapping类的Bean解析、注册、缓存映射关系,并提供匹配执行链的功能
* @param contentNegotiationManager
* @param conversionService
* @param resourceUrlProvider
* @return
*/
@Override
public RequestMappingHandlerMapping requestMappingHandlerMapping(ContentNegotiationManager contentNegotiationManager, FormattingConversionService conversionService, ResourceUrlProvider resourceUrlProvider) {
CustomRequestMappingHandlerMapping customRequestMappingHandlerMapping = new CustomRequestMappingHandlerMapping();
customRequestMappingHandlerMapping.setOrder(1);
return customRequestMappingHandlerMapping;
}

/**
* 自定义匹配的处理器
*/
private static class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping{

/**
* 匹配有@ApiVersion注解的类
* @param handlerType
* @return
*/
@Override
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
return createCondition(apiVersion);
}

/**
* 匹配有@ApiVersion注解的方法
* @param method
* @return
*/
@Override
protected RequestCondition<?> getCustomMethodCondition(Method method) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
return createCondition(apiVersion);
}

/**
* 根据版本号创建匹配条件
* @param apiVersion
* @return
*/
private RequestCondition<ApiVersionCondition> createCondition(ApiVersion apiVersion){
return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value());
}
}

/**
* 当设置WebMvcConfigurationSupport配置时,WebMvcAutoConfiguration则会失效,导致静态资源无法方法(No mapping for get),
* 所以将静态资源添加进MVC容器中
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
super.addResourceHandlers(registry);
if (!registry.hasMappingForPattern("/**")) {
registry.addResourceHandler("/**")
.addResourceLocations(
"classpath:/META-INF/resources/",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
);
}
}
}

3.4 创建controller

新建(v1)版本控制器: src/main/java/com/hui/apiversion/controller/v1/TestController.java

package com.hui.apiversion.controller.v1;

import com.hui.apiversion.annotion.ApiVersion;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController("TestControllerV1")
@ApiVersion
@RequestMapping("{version}/test")
public class TestController {
@RequestMapping("/index")
public String index(){
return "v1 - index -> " + System.currentTimeMillis();
}

/**
* 新版本不存在时,会调用此方法,如: http://127.0.0.1:8080/v2/test/extend
* @return
*/
@RequestMapping("/extend")
public String extend(){
return "v1 - extend -> " + System.currentTimeMillis();
}
}

新建(v2)版本控制器: src/main/java/com/hui/apiversion/controller/v2/TestController.java

package com.hui.apiversion.controller.v2;

import com.hui.apiversion.annotion.ApiVersion;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController("TestControllerV2")
@ApiVersion(2)
@RequestMapping("{version}/test")
public class TestController {
/**
* 覆盖老版本的test/index方法
* @return
*/
@RequestMapping("index")
public String index(){
return "v2 - index -> "+ System.currentTimeMillis();
}
}

当控制器名字一样时,需要通过@RestController("?") 来设置唯一值,否则报错: ...Annotation-specified bean name 'testController' for bean class .. conflicts with existing, non-compatible bean definition of same name and class ...

3.5 请求测试

image-20201111161726170
image-20201111161756067
image-20201111161852855

image-20201111162056401

image-20201111162205655

4.查看源码

上述代码地址(https://github.com/java-item/api-version)