Api接口版本管理实现

引言

日常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 的数据,符合预期。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值