SpringBoot接口 - 如何提供多个版本接口

springboot如何通过注解优雅实现接口多版本管理 阅读详情
在以SpringBoot开发Restful接口时,由于模块,系统等业务的变化,需要对同一接口提供不同版本的参数实现(老的接口还有模块或者系统在用,不能直接改,所以需要不同版本)。如何更加优雅的实现多版本接口呢?

为什么接口会出现多个版本?

为什么接口会出现多个版本?

一般来说,Restful API接口是提供给其它模块,系统或是其他公司使用,不能随意频繁的变更。然而,需求和业务不断变化,接口和参数也会发生相应的变化。如果直接对原来的接口进行修改,势必会影响线其他系统的正常运行。这就必须对api 接口进行有效的版本控制。

有哪些控制接口多版本的方式?

  • 相同URL,用不同的版本参数区分

  • api.pdai.tech/user?version=v1 表示 v1版本的接口, 保持原有接口不动

  • api.pdai.tech/user?version=v2 表示 v2版本的接口,更新新的接口

  • 区分不同的接口域名,不同的版本有不同的子域名, 路由到不同的实例:

  • v1.api.pdai.tech/user 表示 v1版本的接口, 保持原有接口不动, 路由到instance1

  • v2.api.pdai.tech/user 表示 v2版本的接口,更新新的接口, 路由到instance2

  • 网关路由不同子目录到不同的实例(不同package也可以)

  • api.pdai.tech/v1/user 表示 v1版本的接口, 保持原有接口不动, 路由到instance1

  • api.pdai.tech/v2/user 表示 v2版本的接口,更新新的接口, 路由到instance2

  • 同一实例,用注解隔离不同版本控制

  • api.pdai.tech/v1/user 表示 v1版本的接口, 保持原有接口不动,匹配@ApiVersion("1")的handlerMapping

  • api.pdai.tech/v2/user 表示 v2版本的接口,更新新的接口,匹配@ApiVersion("2")的handlerMapping

这里主要展示第四种单一实例中如何优雅的控制接口的版本。

实现案例

这个例子基于SpringBoot封装了@ApiVersion注解方式控制接口版本。

自定义@ApiVersion注解

import org.springframework.web.bind.annotation.Mapping;

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
    String value();
}

定义版本匹配RequestCondition

版本匹配支持三层版本

  • v1.1.1 (大版本.小版本.补丁版本)

  • v1.1 (等同于v1.1.0)

  • v1 (等同于v1.0.0)

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.mvc.condition.RequestCondition;

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

@Slf4j
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {

    /**
     * support v1.1.1, v1.1, v1; three levels .
     */
    private static final Pattern VERSION_PREFIX_PATTERN_1 = Pattern.compile("/v\\d\\.\\d\\.\\d/");
    private static final Pattern VERSION_PREFIX_PATTERN_2 = Pattern.compile("/v\\d\\.\\d/");
    private static final Pattern VERSION_PREFIX_PATTERN_3 = Pattern.compile("/v\\d/");
    private static final List<Pattern> VERSION_LIST = Collections.unmodifiableList(
            Arrays.asList(VERSION_PREFIX_PATTERN_1, VERSION_PREFIX_PATTERN_2, VERSION_PREFIX_PATTERN_3)
    );

    @Getter
    private final String apiVersion;

    public ApiVersionCondition(String apiVersion) {
        this.apiVersion = apiVersion;
    }

    /**
     * method priority is higher then class.
     *
     * @param other other
     * @return ApiVersionCondition
     */
    @Override
    public ApiVersionCondition combine(ApiVersionCondition other) {
        return new ApiVersionCondition(other.apiVersion);
    }

    @Override
    public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
        for (int vIndex = 0; vIndex < VERSION_LIST.size(); vIndex++) {
            Matcher m = VERSION_LIST.get(vIndex).matcher(request.getRequestURI());
            if (m.find()) {
                String version = m.group(0).replace("/v", "").replace("/", "");
                if (vIndex == 1) {
                    version = version + ".0";
                } else if (vIndex == 2) {
                    version = version + ".0.0";
                }
                if (compareVersion(version, this.apiVersion) >= 0) {
                    log.info("version={}, apiVersion={}", version, this.apiVersion);
                    return this;
                }
            }
        }
        return null;
    }

    @Override
    public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
        return compareVersion(other.getApiVersion(), this.apiVersion);
    }

    private int compareVersion(String version1, String version2) {
        if (version1 == null || version2 == null) {
            throw new RuntimeException("compareVersion error:illegal params.");
        }
        String[] versionArray1 = version1.split("\\.");
        String[] versionArray2 = version2.split("\\.");
        int idx = 0;
        int minLength = Math.min(versionArray1.length, versionArray2.length);
        int diff = 0;
        while (idx < minLength
                && (diff = versionArray1[idx].length() - versionArray2[idx].length()) == 0
                && (diff = versionArray1[idx].compareTo(versionArray2[idx])) == 0) {
            ++idx;
        }
        diff = (diff != 0) ? diff : versionArray1.length - versionArray2.length;
        return diff;
    }
}

定义HandlerMapping

import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.lang.reflect.Method;

public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    /**
     * add @ApiVersion to controller class.
     *
     * @param handlerType handlerType
     * @return RequestCondition
     */
    @Override
    protected RequestCondition<?> getCustomTypeCondition(@NonNull Class<?> handlerType) {
        ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
        return null == apiVersion ? super.getCustomTypeCondition(handlerType) : new ApiVersionCondition(apiVersion.value());
    }

    /**
     * add @ApiVersion to controller method.
     *
     * @param method method
     * @return RequestCondition
     */
    @Override
    protected RequestCondition<?> getCustomMethodCondition(@NonNull Method method) {
        ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
        return null == apiVersion ? super.getCustomMethodCondition(method) : new ApiVersionCondition(apiVersion.value());
    }

}

配置注册HandlerMapping

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

@Configuration
public class CustomWebMvcConfiguration extends WebMvcConfigurationSupport {

    @Override
    public RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
        return new ApiVersionRequestMappingHandlerMapping();
    }
}

或者实现WebMvcRegistrations的接口

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer, WebMvcRegistrations {
    //...

    @Override
    @NonNull
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new ApiVersionRequestMappingHandlerMapping();
    }

}

测试运行

controller

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.pdai.springboot.api.version.config.version.ApiVersion;
import tech.pdai.springboot.api.version.entity.User;

/**
 * @author pdai
 */
@RestController
@RequestMapping("api/{v}/user")
public class UserController {

    @RequestMapping("get")
    public User getUser() {
        return User.builder().age(18).name("pdai, default").build();
    }

    @ApiVersion("1.0.0")
    @RequestMapping("get")
    public User getUserV1() {
        return User.builder().age(18).name("pdai, v1.0.0").build();
    }

    @ApiVersion("1.1.0")
    @RequestMapping("get")
    public User getUserV11() {
        return User.builder().age(19).name("pdai, v1.1.0").build();
    }

    @ApiVersion("1.1.2")
    @RequestMapping("get")
    public User getUserV112() {
        return User.builder().age(19).name("pdai2, v1.1.2").build();
    }
}

输出

http://localhost:8080/api/v1/user/get
// {"name":"pdai, v1.0.0","age":18}

http://localhost:8080/api/v1.1/user/get
// {"name":"pdai, v1.1.0","age":19}

http://localhost:8080/api/v1.1.1/user/get
// {"name":"pdai, v1.1.0","age":19} 匹配比1.1.1小的中最大的一个版本号

http://localhost:8080/api/v1.1.2/user/get
// {"name":"pdai2, v1.1.2","age":19}

http://localhost:8080/api/v1.2/user/get
// {"name":"pdai2, v1.1.2","age":19} 匹配最大的版本号,v1.1.2

这样,如果我们向另外一个模块提供v1版本的接口,新的需求中只变动了一个接口方法,这时候我们只需要增加一个接口添加版本号v1.1即可用v1.1版本访问所有接口。

此外,这种方式可能会导致v3版本接口没有发布,但是是可以通过v3访问接口的;这种情况下可以添加一些限制版本的逻辑,比如最大版本,版本集合等。

示例源码

https://download.youkuaiyun.com/download/DeveloperFire/87554658


推荐阅读

Java面试系列-MySQL

JAVA面试汇总(精选系列)

SpringBoot关于接口版本的一个处理方法 作为一个程序猿,对应项目需求的变更似乎已经免疫,加班。加班能解决一切! 有一种需求,接口的升级,即某个接口过时了,不用了,但是却不能删除,需要创建个新的接口,且访问url仅仅只是将api/v1/test改成api/v2/test,SpringBoot在没做处理前,是不允许同一个url访问路径的,在启动时就会报错,当然这是没做处理,处理其实很简单,我们来看一段代码: @ApiOperati... 阅读详情

相关推荐

SpringBoot第15讲:SpringBoot如何提供多个版本接口

本文是SpringBoot第15讲,在以SpringBoot开发Restful接口时,由于模块,系统等业务的变化,需要对同一接口提供不同版本的参数实现(老的接口还有模块或者系统在用,不能直接改,所以需要不同版本)。如何更加优雅的实现多版本接口呢?

Jet_qi的博客 1248

实用:优雅设计接口版本号

一般来说,系统上线以后,需求仍会发生变动,功能也会迭代更新。可能是接口参数发生变更,也有可能是业务逻辑需要调整,如果直接在原来的接口上进行修改,必然会影响原有服务的正常运行。 常见的解决方案,是在接口路径中加入版本号用于区分,此外还可以在参数甚至 header 里带上版本号。这里以在请求路径中带上版本号为例,如:http://IP:PORT/api/v1/test ,v1 即代表的是版本号。当然了,可以像这样,直接写死在 @RequestMapping(“api/v1/test”)属性中,不过下面提供了更为

ZERO 2053

电子产品设计中EFT防护的关键技术与实践

本文深入探讨了电子产品设计中EFT(快速瞬变脉冲群)防护的关键技术与实践。文章从EFT干扰的原理入手,系统阐述了通过优化PCB布局与地平面设计、构建电源与信号端口滤波防护电路、精选核心器件以及实施软件容错策略,来构建多层次、系统性的抗干扰防线,助力产品稳定通过严苛的电磁兼容测试。

e4f5g6h的博客 788

桌面端应用接口版本兼容设计和使用原则

早期开发时,由于选型架构等问题,老旧接口仍然要正常运行,需要逐步引导客户使用新版,但是接口由新团队进行开发,比如 PHP 转成了用 Go 来开发,响应更快,并发更大,新的接口又和老的接口逻辑相近,但传入传出都不相同,这种巨大差异,不应该在原版接口中改动,容易产生灾难性的影响,因此,我们会在 url 上进行处理,例如 url 中接口版本为/api/v1,等到新接口采用/api/v2,这样就有效的改善了新版用户的无缝衔接,接口版本非常少,如果公司接口版本搞得很多,绝对出问题了。

王森的博客 2077

深入解析五大ADC芯片架构及其应用场景

本文深入解析了五大ADC芯片架构及其应用场景,包括逐次逼近型ADC、Σ-Δ型ADC、流水线型ADC、闪存型ADC和积分型ADC。通过详细的架构分析和实际应用案例,帮助工程师理解不同ADC芯片的特点和适用场景,提升电路设计效率。

weixin_28702105的博客 357

采购订单定价策略 超详细

‘需求’字段里填写的是一个例程编号(例程是一段判断程序),如果我们在需求中维护了例程编号,那只有这个例程条件被满足时,这个条件表才会被系统自动读取。一部分的条件表只有在凭证项目满足一定的条件时才会被读取。 ‘排斥的’字段如果别勾中的话,那表明如果这个条件表被系统成功读取到数据后,系统将停止这个存取顺序的执行,否则,系统将继续执行下去。 注意:a、如果我们给某个条件类型设定了有效期的话,那我们必须给这个条件类型分配一个存取顺序。 b、不能给一个抬头条件分配存取顺序。 是...

竹大的博客 1万+

spring boot 接口版本控制

对于应用上线后的接口变动,前后端代码更新无法做到同时更新,容易造成报错,影响用户体验。需做好接口版本控制,在前端代码更迭时期保留旧版本接口的服务提供。

localhost 2031

用Matlab机器人工具箱搞定六轴机械臂建模:从DH参数到3D可视化(附完整代码)

本文详细介绍了如何使用Matlab机器人工具箱进行六轴机械臂建模,从DH参数输入到3D可视化全流程解析。通过正运动学验证、逆运动学求解和交互控制等实战技巧,帮助工程师快速掌握机械臂建模核心方法,并提供完整代码示例解决常见问题。

weixin_27372101的博客 395

Api接口版本管理实现

最终的实现类可能是针对以下情况之一:路径匹配,头部匹配,请求参数匹配,可产生MIME匹配,可消费MIME匹配,请求方法匹配,或者是以上各种情况的匹配条件的一个组合。日常Api接口开发中,接口的变动时有发生,同时老接口保留逻辑,这时需要对接口进行版本标记;或此接口对外暴露,而后接口地址映射发生改变,此时不想调用方做出调整,可将老接口地址映射到新接口的处理逻辑中。接口,版本匹配规则为:调用方未指明版本,使用最新版本;调用方指定版本,使用 小于等于此版本接口中最接近的版本接口。返回版本 1 的数据,符合预期。

qq_38245668的博客 1482

checking OpenSSL library version... configure: error: OpenSSL >= 1.1.1 required (have “100020bf 解决方案

 本文主要介绍了checking OpenSSL library version… configure: error: OpenSSL >= 1.1.1 required (have “100020bf (OpenSSL 1.0.2k-fips 26 Jan 2017)”)解决方案,希望能对使用Linux的同学们有所帮助。 文章目录 1. 问题描述 2. 解决方案

weixin_43178406的博客 2万+

PVE虚拟机运行不流畅,出现卡顿。

🏆本文收录于《优快云问答解惑-专业版》专栏,主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!

**My Coding Family** 3065

Mac OS环境下 SpringBoot实现获取Hbase的数据并提供接口

先上结果图: 本文参考:https://my.oschina.net/u/4631230/blog/4671556 一、创建一个SpringBoot的工程,选择Dependencies->Web 我修改了版本为1.0.0,理论上版本无影响 二、创建的工程目录如下: 以上目录中,HelloController和TestOne是测试SpringBoot和Hbase的,在之前的博文中也有介绍,此处不再赘述,其余的是本项目中的分层结构。 三、Hbase的准备 新建简单的表如下: 四、代码详情 4

K的博客 280

42、使用mmrotate中k3det进行旋转目标检测,并进行mnn部署和ncnn部署

基本思想:仍然是身份证分割,并进行ncnn和mnn的c++的部署开发

sxj731533730 5713

专门为中小软件提供的开源JAVA网络验证系统

Verify 1.7.4 项目介绍 专门为中小软件提供的开源JAVA网络验证系统 快速让自己开发的软件拥有以下功能: 目前的功能主要有:注册,登陆,绑定卡密,绑定机器,取软件版本,软件留言,修改密码,取卡密期限,rsa算法加密登陆,取软件信息 后台页面6大模块 接口管理 软件管理 日志管理 充值卡管理 用户管理 配置管理 系统亮点: 开放接口全部可以后台配置,进行ip限流配置管理,限制接口在 xx 分钟内容只能访问 xx 次 开放接口全部进行了严格认证处理 用户登陆注册日志看得见

我的博客 1857

高斯过程的数学理解

G澳大利亚的过程是有益的,特别是当我们有少量数据时。当我在制造业担任数据科学家时,我们的团队使用这种算法来揭示我们接下来应该进行哪些实验条件。但是,此算法不如其他算法流行。在这篇博客中,我将通过可视化和 Python 实现来解释高斯过程 [1] 的数学背景。

gongdiwudu的专栏 3611

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

SpringBoot整合Swagger

swagger版本是2.9.2,因为Springfox使用的路径匹配是基于AntPathMatcher的,而Spring Boot 2.7.X使用的是PathPatternMatcher,版本不适配。5、当扫描的文件中接口的返回值中存在实体类时,就可以发现Swagger中Models存在该实体类,若不存在返回实体类,则不显示Models。2、接口文档实时更新(可以直接访问这个网址查看提供的api信息啊,实时更新)2、配置Swagger的Config。1、下载swagger相关依赖。3、配置Docket。

weixin_47971118的博客 377

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

基于SpringBoot_后端接口流程

本篇博文是本人(初级程序员)基于企业级实战项目后做一点点的记录,一是为了总结自己的开发经历,二是为了将自己所遇到的问题和所解决的办法分享给刚刚入门的各位小伙伴。有实战经验的各位前辈们可以将本人写的内容看成是饭后甜点,不到之处还请批评指正哦!...

Fish_Vast的博客 1万+

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

SpringBoot中自己写的接口中调用别人提供的接口

以请求折线图描点的数据接口为例,记录在自己写的接口中调用同事提供的接口的过程。先看同事给的接口:输入为:customerNum beginDate endDatetypesort具体实现如下:1、service里面写具体调用同事接口的实现方法@Service public interface MssmService { @Headers({"Content-Type: applica...

qq_41827310的博客 1万+

SpringBoot实现接口版本控制

一个系统在上线后会不断迭代更新,需求也会不断变化,有可能接口的参数也会发生变化,如果在原有的参数上直接修改,可能会影响到现有项目的正常运行,这时我们就需要设置不同的版本,这样即使参数发生变化,由于老版本没有变化,因此不会影响上线系统的运行。 这里我们选择使用带有一位小数的浮点数作为版本号,在请求地址末尾中带上版本号,大致的地址如:http://api/test/1.0,其中,1.0即代表的是版本...

Zac的博客 7680

SpringBoot实现API接口多版本支持

点击上方蓝字关注我们!一、简介产品迭代过程中,同一个接口可能同时存在多个版本,不同版本的接口URL、参数相同,可能就是内部逻辑不同。尤其是在同一接口需要同时支持旧版本和新版本的情况下,比...

独行侠梦的博客 3402

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

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

小灰灰blog的专栏 779

SpringBoot中一个接口多个实现类,如何自由选择,这是一把《屠龙宝刀》

一个接口难免会有多个实现类,不同的实现类有各自的需求,如何在项目中实现根据不同项目,自由切换实现类呢? 废话不多说,直接上代码,文末有源码,直接测试 1.接口代码 package com.cj.interfaces; public interface ITestService { void test(); } 2.实现类1,使用@Service,且其中指定bean的名字为“s1” package com.cj.interfaces; import org.springframework.ste

我是Cj,一个正义之人 1万+

SpringBoot 2.X——接口版本控制

一个项目上线后,会根据不同的需求不断的进行更新和维护操作。有可能接口的请求也会发生改变,比如之前 /api/v1 ,后来随着业务变更,需要使用 /api/v9,但有些接口根本就无需变更,一样沿用即可。 如何做到一个接口多个版本可以动态适用呢? 一、自定义一个注解 import java.lang.annotation.Documented; import java.lang.annotation....

专注写bug 3121

SpringBoot同一接口多个实现类配置

SpringBoot项目中可能出现一个接口有多个实现类的情况,如果不进行配置,注入接口时编译器不知道要注入哪个实现类就会报错,因此需要进行配置。以下进行举例: 接口如下: public interface NoticeService { public String noticeUser(Long id); } 两个实现类如下: @Service public class NoticeServiceImpl1 implements NoticeService { public String.

qq_42026590的博客 2619
上一篇: SpringBoot接口 - 如何统一异常处理
下一篇: 基于java+springboot的交易平台
毕设王同学
博客等级 码龄12年 133粉丝 128原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

毕设王同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值