引言
日常Api接口开发中,接口的变动时有发生,同时老接口保留逻辑,这时需要对接口进行版本标记;或此接口对外暴露,而后接口地址映射发生改变,此时不想调用方做出调整,可将老接口地址映射到新接口的处理逻辑中。
实现
因涉及到 请求地址和处理方法的匹配改造,所以需要使用到RequestCondition
进行匹配逻辑的实现。
使用 RequestMappingHandlerMapping
将 condition 和 methodMapping 进行映射。
RequestCondition
RequestCondition
是Spring MVC对一个请求匹配条件的概念建模。最终的实现类可能是针对以下情况之一:路径匹配,头部匹配,请求参数匹配,可产生MIME匹配,可消费MIME匹配,请求方法匹配,或者是以上各种情况的匹配条件的一个组合。
public interface RequestCondition<T> {
/**
* condition 的组合
* 可传入其他类型condition进行组合实现
*/
T combine(T other);
/**
* 获取当前请求匹配的condition
* 如果匹配-返回condition实例,如果匹配不到-返回null
* 这个condition实例是只适用于 当前request
*/
@Nullable
T getMatchingCondition(HttpServletRequest request);
/**
* 对 request 使用 getMatchingCondition 获取到 condition后执行compare
* 比对是针对同一 request ,返回一个比较结果(带有优先级概念)
* 0 -- 最优
*/
int compareTo(T other, HttpServletRequest request);
}
此场景下,会使用 RequestMappingHandlerMapping
将 版本匹配的 condition 和 method 以及 request 进行匹配绑定。
实现代码
ApiVersion注解
使用 ApiVersion
对 Controller 接口进行版本标注。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
/**
* 版本号
* 主要应对 同 url 不同版本
* @return
*/
String value();
/**
* 旧 URL 全拼
* 主要应对 url 发生变化(请求体和对应请求参数不变),但不想通知调用方修改
* 可将 oldFullPath 配置的 全url 映射到此 method 上
* @return
*/
String[] oldFullPath() default {};
}
ApiVersionRequestCondition 版本匹配
实现 RequestCondition
接口,版本匹配规则为:调用方未指明版本,使用最新版本;调用方指定版本,使用 小于等于此版本接口中最接近的版本接口。
public class ApiVersionRequestCondition implements RequestCondition<ApiVersionRequestCondition> {
private static final Logger LOGGER = LoggerFactory.getLogger(ApiVersionRequestCondition.class);
public static final String API_VERSION_HEADER = "Api-Version";
private String version;
/**
* 标记当前是否为最高版本
* 如果调用方未指定版本,则使用最高版本
*/
private boolean maxVersion = false;
public ApiVersionRequestCondition(String version) {
this.version = version;
}
/**
* 合并条件
* 如:类上指定了@RequestMapping的 url 为 root -
* 而方法上指定的@RequestMapping的 url 为 method -
* 那么在获取这个接口的 url 匹配规则时,类上扫描一次,方法上扫描一次,
* 这个时候就需要把这两个合并成一个,表示这个接口匹配root/method
*
* @param other
* @return
*/
@Override
public ApiVersionRequestCondition combine(ApiVersionRequestCondition other) {
return this;
}
/**
* 匹配请求,如果匹配到,返回condition实例
* @param request
* @return
*/
@Override
public ApiVersionRequestCondition getMatchingCondition(HttpServletRequest request) {
String version = request.getHeader(API_VERSION_HEADER);
if (StringUtils.isEmpty(version)) {
// 默认取最新版本
if (maxVersion) {
return this;
}
return null;
}
// 调用方需要的版本大于当前condition版本
if (version.compareTo(this.version) >= 0) {
// 返回当前版本
return this;
}
return null;
}
/**
* 针对指定的请求对象request发现有多个满足条件的,用来排序指定优先级,使用最优的进行响应
* 在 getMatchingCondition 方法中,只会返回 version < requestVersion 的 condition
* 所以最优的condition是,小于 requestVersion ,切最接近 requestVersion 版本
* @param other
* @param request
* @return
*/
@Override
public int compareTo(ApiVersionRequestCondition other, HttpServletRequest request) {
return other.version.compareTo(version);
}
@Override
public int hashCode() {
return version.hashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ApiVersionRequestCondition)) {
return false;
}
return version.equals(((ApiVersionRequestCondition) obj).version);
}
public String getVersion() {
return version;
}
public void setMaxVersion(boolean maxVersion) {
this.maxVersion = maxVersion;
}
}
ApiVersionHandlerMapping 将condition绑定接口method
使用 RequestMappingHandlerMapping
-> getCustomMethodCondition
将condition和method进行绑定,使用 RequestMappingHandlerMapping
-> registerHandlerMethod
进行最高版本的标记和老URL的映射处理。
public class ApiVersionHandlerMapping extends RequestMappingHandlerMapping {
/**
* 存在版本标记的 RequestMapping -- ApiVersionRequestCondition
* 且 只存储 最高版本
*/
private final Map<RequestMappingInfoHashEqual, ApiVersionRequestCondition> apiMaxVersion = new HashMap<>();
@Value("${server.path:}")
private String serverPath;
@Override
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
if (mapping.getCustomCondition() instanceof ApiVersionRequestCondition) {
// 处理版本
handleMaxVersion(mapping);
// 处理老URL映射
handleFullPathMapping(mapping, method, handler);
}
RequestMappingInfo requestMappingInfo = handlePathPrefix(mapping);
super.registerHandlerMethod(handler, method, requestMappingInfo);
}
@Override
protected ApiVersionRequestCondition getCustomMethodCondition(Method method) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
if (apiVersion != null) {
// 存在 ApiVersion 注解,使用ApiVersionRequestCondition
return new ApiVersionRequestCondition(apiVersion.value());
}
// default return null
return null;
}
RequestMappingInfo handlePathPrefix(RequestMappingInfo info) {
if (StringUtils.isEmpty(serverPath)) {
return info;
}
return RequestMappingInfo.paths(serverPath).build().combine(info);
}
void handleMaxVersion(RequestMappingInfo mapping) {
ApiVersionRequestCondition cc = (ApiVersionRequestCondition) mapping.getCustomCondition();
if (cc == null) {
return;
}
// 构建 mapping 可比对的对象,用于判断 两个 RequestMapping 是否相同
RequestMappingInfoHashEqual infoHashEqual = new RequestMappingInfoHashEqual(mapping);
// 判断相同 RequestMapping 是否已存在其他版本标记
ApiVersionRequestCondition versionRequestCondition = apiMaxVersion.get(infoHashEqual);
if (versionRequestCondition == null) {
// 不存在,存储当前 ApiVersionRequestCondition
apiMaxVersion.put(infoHashEqual, cc);
// 设置当前 请求 为 maxVersion
cc.setMaxVersion(true);
} else {
// 比较当前版本和最高版本
if (cc.getVersion().compareTo(versionRequestCondition.getVersion()) >= 0) {
// 大于,最高版本,设置当前版本为最高版本,比较版本取消最高版本标记
cc.setMaxVersion(true);
versionRequestCondition.setMaxVersion(false);
apiMaxVersion.put(infoHashEqual, cc);
}
}
}
void handleFullPathMapping(RequestMappingInfo mapping, Method method, Object handler) {
// 处理 oldFullPath
ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
if (apiVersion == null) {
return;
}
String[] oldFullPath = apiVersion.oldFullPath();
if (oldFullPath.length == 0) {
return;
}
// 手动构建一个 oldFullPath 对应的 RequestMappingInfo
RequestMappingInfo mappingInfo = RequestMappingInfo.paths(oldFullPath).build();
// 将当前method-request-mapping的相关信息,复制到 oldFullPath-mappingInfo
RequestMappingInfo requestMappingInfo = new RequestMappingInfo(mappingInfo.getName(),
RequestMappingInfo.paths(oldFullPath).build().getPatternsCondition(),
mapping.getMethodsCondition(), mapping.getParamsCondition(),
mapping.getHeadersCondition(), mapping.getConsumesCondition(),
mapping.getProducesCondition(), null);
// 注册 oldFullPath-mappingInfo
super.registerHandlerMethod(handler, method, requestMappingInfo);
}
static class RequestMappingInfoHashEqual {
private final PatternsRequestCondition patternsCondition;
private final RequestMethodsRequestCondition methodsCondition;
private final ParamsRequestCondition paramsCondition;
private final HeadersRequestCondition headersCondition;
private final ConsumesRequestCondition consumesCondition;
private final ProducesRequestCondition producesCondition;
public RequestMappingInfoHashEqual(RequestMappingInfo mappingInfo) {
patternsCondition = mappingInfo.getPatternsCondition();
methodsCondition = mappingInfo.getMethodsCondition();
paramsCondition = mappingInfo.getParamsCondition();
headersCondition = mappingInfo.getHeadersCondition();
consumesCondition = mappingInfo.getConsumesCondition();
producesCondition = mappingInfo.getProducesCondition();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
RequestMappingInfoHashEqual that = (RequestMappingInfoHashEqual) o;
return Objects.equals(patternsCondition, that.patternsCondition) &&
Objects.equals(methodsCondition, that.methodsCondition) &&
Objects.equals(paramsCondition, that.paramsCondition) &&
Objects.equals(headersCondition, that.headersCondition) &&
Objects.equals(consumesCondition, that.consumesCondition) &&
Objects.equals(producesCondition, that.producesCondition);
}
@Override
public int hashCode() {
return Objects.hash(patternsCondition, methodsCondition, paramsCondition, headersCondition, consumesCondition, producesCondition);
}
}
}
使用及案例测试
测试Controller 编写
编写3个接口,分别对应版本 0、1、2,进行版本测试。
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/v")
@ApiVersion("0")
public ApiResult<?> v1_0() {
return ApiResult.success("V0");
}
@GetMapping("/v")
@ApiVersion("1")
public ApiResult<?> v1_1() {
return ApiResult.success("V1");
}
@GetMapping("/v")
@ApiVersion("2")
public ApiResult<?> v1_2() {
return ApiResult.success("V2");
}
}
1、默认不传 Api-Version 版本
返回最大版本 2,符合预期。
2、传入指定版本 1
返回版本 1 的数据,符合预期。
3、传入 1.5 版本号,查看是否返回 版本 1 数据
返回版本 1 的数据,符合预期。