Api接口版本管理实现

2023-11-08

引言

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Api接口版本管理实现 的相关文章

随机推荐

  • dcdc升压计算器excel_优秀DCDC升压

    Figure 1 Basic Application Circuit GENERAL DESCRIPTION The MT3608 is a constant frequency 6 pin SOT23 current mode step
  • pycharm简单使用(Mac):创建一个helloWord

    说明 VSCode是一款轻量级的开发工具 可以支持多款插件这个学习使用确实是一个好的工具 PyCharm是一款Python专门支持的IDE 为什么这里要使用PyCharm呢 PyCharm支持断点调试 1 第一步 创建一个项目 2 第二步
  • CUDA 基础 01 - 概念

    最近在GPU编译器测试方面遇到一些瓶颈 准备学习下cuda 相关的基础知识 warp sm index grid等 CPU VS GPU GPU最重要的一点是可以并行的实现数据处理 这一点在数据量大 运算复杂度不高的条件下极为适用 可以简单
  • 3 Ubuntu上使用Qt运行多线程,设置程序自启动及保护脚本

    Ubuntu上使用Qt运行多线程 设置程序自启动及保护脚本 多线程 自启动及保护脚本 自启动及保护脚本 结束自启动脚本 脚本程序简单说明 设置自启动 多线程 使用多线程时我们需要加入 include lt thread gt 这个头文件包含
  • 区块链常见的几大共识机制

    区块链技术被广泛应用于许多领域 其中共识机制是构成区块链系统的核心部分 共识机制是指用来维护区块链数据一致性 安全性和可靠性的机制 常见的区块链共识机制有以下几种 1 工作量证明 Proof of Work 是最早的共识机制 它将矿工通过解
  • 毕业设计-基于机器视觉的交通标志识别系统

    目录 前言 课题背景和意义 实现技术思路 一 交通标志识别系统 二 交通标志识别整体方案 三 实验分析 四 总结 实现效果图样例 最后 前言 大四是整个大学期间最忙碌的时光 一边要忙着备考或实习为毕业后面临的就业升学做准备 一边要为毕业设计
  • 开源项目Tinyhttp项目源码详解

    HTTP协议 http协议叫做超文本传输协议 是基于tcp ic协议的应用层协议 具体内容可以借鉴这一篇博客 https blog csdn net qq 36894974 article details 103930478 本文主要涉及T
  • [人工智能-深度学习-33]:卷积神经网络CNN - 常见分类网络- LeNet网络结构分析与详解

    作者主页 文火冰糖的硅基工坊 文火冰糖 王文兵 的博客 文火冰糖的硅基工坊 CSDN博客 本文网址 https blog csdn net HiWangWenBing article details 120893764 目录 第1章 卷积神
  • Ubuntu14.04安装配置NFS用于挂载嵌入式文件系统

    Ubuntu14 04安装配置NFS用于挂载嵌入式文件系统 1 安装 sudo apt get install nfs kernel server rpcbind 2 配置 vi etc exports 在文件的最后一行加上 yaffs2
  • 获取随机位数阿拉伯数字

    int Math random 9 1 1000 这里是随机4位数 需要几位数 就乘以几个零 int Math random 9 1 100 随机3位数 int Math random 9 1 10 随机2位数 来个方法吧 获取随机位数的阿
  • IPSec 基础介绍

    IPSec是IETF Internet Engineering Task Force 制定的一组开放的网络安全协议 它并不是一个单独的协议 而是一系列为IP网络提供安全性的协议和服务的集合 包括认证头AH Authentication He
  • python TimedRotatingFileHandler 配置参数 (转)

    TimedRotatingFileHandler这个模块是满足文件名按时间自动更换的需求 这样就可以保证日志单个文件不会太大 用法很简单 示例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 impo
  • python学习之【模块】

    前言 上一篇文章 python学习之 深拷贝 中学习了python中的深浅拷贝学习内容 这篇文章接着学习python中的模块 什么是模块 在python中 一个文件 以 py 为后缀名的文件 就叫做一个模块 每一个模块在python里都被看
  • 群晖做网页服务器_群晖NAS软件DS get介绍及使用方法教程

    我的NAS介绍第二篇 群晖NAS软件介绍与应用之DS get篇前言 1 为什么选择NAS之所以我现在建议大家选择NAS 不仅仅因为网盘的不稳定性和不安全性 遇到和谐大神不说 网盘也经历了各种风风雨雨 从和谐到倒闭不过一步之遥 大家都懂的 还
  • Mysql-连接https域名的Mysql数据源踩的坑

    背景介绍 大家在实际项目中 大部分都会用到关系数据库mysql 通常数据库服务器提供的都是ip的方式 所以不会出现本文涉及到的https域名的问题 本文介绍的是基于数据库服务器是分配了指定域名且有ssl证书的https 连接数据源 遇到的问
  • java面向对象基础练习--实现简单的图书管理系统

    这个系统使用的是java的基础语法 没有使用数据库 实现图书管理系统基础的查询 增加 删除 借阅 归还 打印 退出功能 这个小项目作为我java基础语法的综合运用 主要是为了建立面向对象编程的思想 培养编程习惯 如果有错误或者更好的实现方法
  • 深入详解ThreadLocal

    本文已收录至GitHub 推荐阅读 Java随想录 微信公众号 Java随想录 原创不易 注重版权 转载请注明原作者和原文链接 文章目录 什么是ThreadLocal ThreadLocal 原理 set方法 get方法 remove方法
  • 又回来了

    又回来了 一年多没有来了 再次回来还是感觉那么熟悉 那么亲切 怀念以前在学校的日子 怀念苦苦思索技术的日子 除了学习没有繁杂的社会关系要处理 单纯快乐着 为了一个小小的技术难题 愿意不吃不喝去摸索 去测试 在成功的那一刻忘记了一切疲倦和劳累
  • c++ 实现数据库连接池

    c 实现数据库连接池 自己尝试用c 新标准实现了数据库连接池 代码简化了很多 思路 将数据库的连接当作一个对象添加进list队列中 在连接池创建的时候就建立好队列 并添加自定义大小的连接对象 连接对象用智能指针来管理 现代c 中不应该出现d
  • Api接口版本管理实现

    Api接口版本管理实现 引言 实现 RequestCondition 实现代码 ApiVersion注解 ApiVersionRequestCondition 版本匹配 ApiVersionHandlerMapping 将condition