SpringBoot 应用篇 实现后端的接口版本支持

简介: 作为一个主职的后端开发者,在平时的工作中,最讨厌的做的事情可以说是参数校验和接口的版本支持了。对于客户端的同学来说,业务的历史包袱会小很多,当出现不兼容的业务变动时,直接开发新的就好;然而后端就没有这么简单了,历史的接口得支持,新的业务也得支持,吭哧吭哧的新加一个服务接口,url 又不能和之前的相同,怎么办?只能在某个地方加一个类似v1, v2...那么有没有一种不改变 url,通过其他的方式来支持版本管理的方式呢?

image.png


作为一个主职的后端开发者,在平时的工作中,最讨厌的做的事情可以说是参数校验和接口的版本支持了。对于客户端的同学来说,业务的历史包袱会小很多,当出现不兼容的业务变动时,直接开发新的就好;然而后端就没有这么简单了,历史的接口得支持,新的业务也得支持,吭哧吭哧的新加一个服务接口,url 又不能和之前的相同,怎么办?只能在某个地方加一个类似v1, v2...


那么有没有一种不改变 url,通过其他的方式来支持版本管理的方式呢?


本文将介绍一种,利用请求头来传递客户端版本,在相同的 url 中寻找最适合的这个版本请求的接口的实例 case


主要用到的知识点为:


  • RequestCondition
  • RequestMappingHandlerMapping


I. 应用场景



我们希望同一个业务始终用相同的 url,即便不同的版本之间业务完全不兼容,通过请求参数中的版本选择最合适的后端接口来响应这个请求


1. 约定


需要实现上面的 case,首先有两个约定


  • 每个请求中必须携带版本参数
  • 每个接口都定义有一个支持的版本


2. 规则


明确上面两点前提之后,就是基本规则了


版本定义


根据常见的三段式版本设计,版本格式定义如下


x.x.x
复制代码


  • 其中第一个 x:对应的是大版本,一般来说只有较大的改动升级,才会改变
  • 其中第二个 x:表示正常的业务迭代版本号,每发布一个常规的 app 升级,这个数值+1
  • 最后一个 x:主要针对 bugfix,比如发布了一个 app,结果发生了异常,需要一个紧急修复,需要再发布一个版本,这个时候可以将这个数值+1


接口选择


通常的 web 请求都是通过 url 匹配规则来选择对应响应接口,但是在我们这里,一个 url,可能会有多个不同的接口,该怎么选择呢?


  • 首先从请求中,获取版本参数 version
  • 从所有相同的 url 接口中,根据接口上定义的版本,找到所有小于等于 version 的接口
  • 在上面满足条件的接口中,选择版本最大的接口来响应请求


II. 应用实现



明确上面的应用场景之后,开始设计与实现


1. 接口定义


首先我们需要一个版本定义的注解,用于标记 web 服务接口的版本,默认版本好为 1.0.0


@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Api {
    /**
     * 版本
     *
     * @return
     */
    String value() default "1.0.0";
}
复制代码


其次需要一个版本对应的实体类,注意下面的实现中,默认版本为1.0.0,并实现了Comparable接口,支持版本之间的比较


@Data
public class ApiItem implements Comparable<ApiItem> {
    private int high = 1;
    private int mid = 0;
    private int low = 0;
    public ApiItem() {
    }
    @Override
    public int compareTo(ApiItem right) {
        if (this.getHigh() > right.getHigh()) {
            return 1;
        } else if (this.getHigh() < right.getHigh()) {
            return -1;
        }
        if (this.getMid() > right.getMid()) {
            return 1;
        } else if (this.getMid() < right.getMid()) {
            return -1;
        }
        if (this.getLow() > right.getLow()) {
            return 1;
        } else if (this.getLow() < right.getLow()) {
            return -1;
        }
        return 0;
    }
}
复制代码


需要一个将 string 格式的版本转换为 ApiItem 的转换类,并且支持了默认版本为1.0.0的设定


public class ApiConverter {
    public static ApiItem convert(String api) {
        ApiItem apiItem = new ApiItem();
        if (StringUtils.isBlank(api)) {
            return apiItem;
        }
        String[] cells = StringUtils.split(api, ".");
        apiItem.setHigh(Integer.parseInt(cells[0]));
        if (cells.length > 1) {
            apiItem.setMid(Integer.parseInt(cells[1]));
        }
        if (cells.length > 2) {
            apiItem.setLow(Integer.parseInt(cells[2]));
        }
        return apiItem;
    }
}
复制代码


2. HandlerMapping 接口选择


需要一个 url,支持多个请求接口,可以考虑通过RequestCondition来实现,下面是具体的实现类


public class ApiCondition implements RequestCondition<ApiCondition> {
    private ApiItem version;
    public ApiCondition(ApiItem version) {
        this.version = version;
    }
    @Override
    public ApiCondition combine(ApiCondition other) {
        // 选择版本最大的接口
        return version.compareTo(other.version) >= 0 ? new ApiCondition(version) : new ApiCondition(other.version);
    }
    @Override
    public ApiCondition getMatchingCondition(HttpServletRequest request) {
        String version = request.getHeader("x-api");
        ApiItem item = ApiConverter.convert(version);
        // 获取所有小于等于版本的接口
        if (item.compareTo(this.version) >= 0) {
            return this;
        }
        return null;
    }
    @Override
    public int compareTo(ApiCondition other, HttpServletRequest request) {
        // 获取最大版本对应的接口
        return other.version.compareTo(this.version);
    }
}
复制代码


虽然上面的实现比较简单,但是有必要注意一下两个逻辑


  • getMatchingCondition方法中,控制了只有版本小于等于请求参数中的版本的 ApiCondition 才满足规则
  • compareTo 指定了当有多个ApiCoondition满足这个请求时,选择最大的版本


自定义RequestMappingHandlerMapping实现类ApiHandlerMapping


public class ApiHandlerMapping extends RequestMappingHandlerMapping {
    @Override
    protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
        return buildFrom(AnnotationUtils.findAnnotation(handlerType, Api.class));
    }
    @Override
    protected RequestCondition<?> getCustomMethodCondition(Method method) {
        return buildFrom(AnnotationUtils.findAnnotation(method, Api.class));
    }
    private ApiCondition buildFrom(Api platform) {
        return platform == null ? new ApiCondition(new ApiItem()) :
                new ApiCondition(ApiConverter.convert(platform.value()));
    }
}
复制代码


注册

@Configuration
public class ApiAutoConfiguration implements WebMvcRegistrations {
    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new ApiHandlerMapping();
    }
}
复制代码


基于此,一个实现接口版本管理的微框架已经完成;接下来进入测试环节


III. 测试



case1. 方法上添加版本


设计三个接口,一个不加上注解,两外两个添加不同版本的注解


@RestController
@RequestMapping(path = "v1")
public class V1Rest {
    @GetMapping(path = "show")
    public String show1() {
        return "v1/show 1.0.0";
    }
    @Api("1.1.2")
    @GetMapping(path = "show")
    public String show2() {
        return "v1/show 1.1.2";
    }
    @Api("1.1.0")
    @GetMapping(path = "show")
    public String show3() {
        return "v1/show 1.1.0";
    }
}
复制代码


在发起请求时,分别不带上版本,带指定版本,来测试对应的响应


image.png


  • 从上面的截图可以看出,请求头中没有版本时,默认给一个1.0.0的版本
  • 响应的是小于请求版本的接口中,版本最大的哪一个


case2. 类版本+方法版本


每个方法上添加版本有点蛋疼,在上面的注解定义中,就支持了类上注解,从实现上也可以看出,当方法和类上都有注解时,选择最大的版本


@Api("2.0.0")
@RestController
@RequestMapping(path = "v2")
public class V2Rest {
    @Api("1.1.0")
    @GetMapping(path = "show")
    public String show0() {
        return "v2/show0 1.1.0";
    }
    @GetMapping(path = "show")
    public String show1() {
        return "v2/show1 2.0.0";
    }
    @Api("2.1.1")
    @GetMapping(path = "show")
    public String show2() {
        return "v2/show2 2.1.1";
    }
    @Api("2.2.0")
    @GetMapping(path = "show")
    public String show3() {
        return "v2/show3 2.2.0";
    }
}
复制代码


根据我们的实现规则,show0 和 show1 都会相应 <2.1.1 的版本请求,这个时候会出现冲突;


image.png

  • 从上面的截图中,可以看出来版本小于 2.0.0 的请求,报的是 404 错误
  • 请求版本小于 2.1.1 的请求,报的是冲突异常



相关文章
|
5月前
|
XML Java Nacos
Spring Boot 整合Nacos 版本兼容适配 史上最详细文档
本文介绍SpringBoot整合Nacos的完整流程,涵盖Nacos下载安装、配置中心与服务发现集成、版本兼容性问题及实战配置。重点解决SpringBoot 3.3.0与Nacos版本适配难题,推荐使用Spring Cloud Alibaba方案,并提供项目开源地址供参考学习。
|
5月前
|
安全 NoSQL Java
SpringBoot接口安全:限流、重放攻击、签名机制分析
本文介绍如何在Spring Boot中实现API安全机制,涵盖签名验证、防重放攻击和限流三大核心。通过自定义注解与拦截器,结合Redis,构建轻量级、可扩展的安全防护方案,适用于B2B接口与系统集成。
762 3
|
5月前
|
安全 Java Ruby
我尝试了所有后端框架 — — 这就是为什么只有 Spring Boot 幸存下来
作者回顾后端开发历程,指出多数框架在生产环境中难堪重负。相比之下,Spring Boot凭借内置安全、稳定扩展、完善生态和企业级支持,成为构建高可用系统的首选,真正经受住了时间与规模的考验。
393 2
|
9月前
|
JavaScript 前端开发 Java
制造业ERP源码,工厂ERP管理系统,前端框架:Vue,后端框架:SpringBoot
这是一套基于SpringBoot+Vue技术栈开发的ERP企业管理系统,采用Java语言与vscode工具。系统涵盖采购/销售、出入库、生产、品质管理等功能,整合客户与供应商数据,支持在线协同和业务全流程管控。同时提供主数据管理、权限控制、工作流审批、报表自定义及打印、在线报表开发和自定义表单功能,助力企业实现高效自动化管理,并通过UniAPP实现移动端支持,满足多场景应用需求。
854 1
|
8月前
|
算法 网络协议 Java
Spring Boot 的接口限流算法
本文介绍了高并发系统中流量控制的重要性及常见的限流算法。首先讲解了简单的计数器法,其通过设置时间窗口内的请求数限制来控制流量,但存在临界问题。接着介绍了滑动窗口算法,通过将时间窗口划分为多个格子,提高了统计精度并缓解了临界问题。随后详细描述了漏桶算法和令牌桶算法,前者以固定速率处理请求,后者允许一定程度的流量突发,更符合实际需求。最后对比了各算法的特点与适用场景,指出选择合适的算法需根据具体情况进行分析。
706 56
Spring Boot 的接口限流算法
|
10月前
|
前端开发 Java 物联网
智慧班牌源码,采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署
智慧班牌系统是一款基于信息化与物联网技术的校园管理工具,集成电子屏显示、人脸识别及数据交互功能,实现班级信息展示、智能考勤与家校互通。系统采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署与私有化定制。核心功能涵盖信息发布、考勤管理、教务处理及数据分析,助力校园文化建设与教学优化。其综合性和可扩展性有效打破数据孤岛,提升交互体验并降低管理成本,适用于日常教学、考试管理和应急场景,为智慧校园建设提供全面解决方案。
585 70
|
8月前
|
Java API 网络架构
基于 Spring Boot 框架开发 REST API 接口实践指南
本文详解基于Spring Boot 3.x构建REST API的完整开发流程,涵盖环境搭建、领域建模、响应式编程、安全控制、容器化部署及性能优化等关键环节,助力开发者打造高效稳定的后端服务。
1120 1
|
9月前
|
开发框架 Java 关系型数据库
在Linux系统中安装JDK、Tomcat、MySQL以及部署J2EE后端接口
校验时,浏览器输入:http://[your_server_IP]:8080/myapp。如果你看到你的应用的欢迎页面,恭喜你,一切都已就绪。
594 17
|
9月前
|
Java 关系型数据库 MySQL
在Linux操作系统上设置JDK、Tomcat、MySQL以及J2EE后端接口的部署步骤
让我们总结一下,给你的Linux操作系统装备上最强的军队,需要先后装备好JDK的弓箭,布置好Tomcat的阵地,再把MySQL的物资原料准备好,最后部署好J2EE攻城车,那就准备好进军吧,你的Linux军团,无人可挡!
209 18

热门文章

最新文章