springboot如何通过注解优雅实现接口多版本管理

springboot如何通过注解优雅实现接口多版本管理

背景

一个web服务一旦上线后接口往往对接很多下游,下游应用可能强依赖这个接口,因此如果对接口进行版本更新最好是要兼容过去版本,提供一个相同功能的新版本。

接口版本管理的方式

  1. 通过请求参数实现
yunfei.group/user?version=v1 表示 v1版本的接口, 保持原有接口不动
yunfei.group/user?version=v2 表示 v2版本的接口,更新新的接口
  1. 通过域名实现(新版本接口在新实例,通过域名路由过去)
v1.yunfei.group/user
v2.yunfei.group/user
  1. 注解实现,通过重定义Handler Mapping过程实现。

注解方式实现接口版本管理

要实现的效果:

  1. 当没有匹配上版本时走默认接口
  2. 当有完全对应的版本时走完全匹配的接口
  3. 当没有完全对应的版本时走对应的最新版本

版本的定义:

  • v1.1.1 (大版本.小版本.补丁版本)
  • v1.1 (等同于v1.1.0)
  • v1 (等同于v1.0.0)

代码实现

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.6.3</version><relativePath/></parent><groupId>org.example</groupId><artifactId>ApiVersionDemo</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-aop</artifactId></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-simple</artifactId></dependency></dependencies></project>

自定义注解

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

这里value就是接口的版本号。

配置注册HandlerMapping

@Configuration
public class CustomWebMvcConfiguration extends WebMvcConfigurationSupport {@Overridepublic RequestMappingHandlerMapping createRequestMappingHandlerMapping() {return new ApiVersionRequestMappingHandlerMapping();}
}

定义ApiVersionRequestMappingHandlerMapping

public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {/*** add @ApiVersion to controller class.** @param handlerType handlerType* @return RequestCondition*/@Overrideprotected 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*/@Overrideprotected RequestCondition<?> getCustomMethodCondition(@NonNull Method method) {ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);return null == apiVersion ? super.getCustomMethodCondition(method) : new ApiVersionCondition(apiVersion.value());}}

springboot会在启动的时候分别针对controller和其中的method分别调用上述getCustomTypeCondition和getCustomMethodCondition方法,当没有注解在controller或者method上时RequestCondition为默认,否则为ApiVersionCondition,并将对应注解的版本号传入。

定义版本匹配ApiVersionCondition

这里compareTo函数是在有多个符合条件的接口时用于比较接口匹配度的方法,这里定义为符合条件里最新的版本为匹配版本。compareVersion函数主要定义了两个版本比较谁大谁小的逻辑。getMatchingCondition为核心函数,用来判断请求和当前接口是否匹配。

@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));@Getterprivate final String apiVersion;public ApiVersionCondition(String apiVersion) {this.apiVersion = apiVersion;}/*** method priority is higher then class.** @param other other* @return ApiVersionCondition*/@Overridepublic ApiVersionCondition combine(ApiVersionCondition other) {return new ApiVersionCondition(other.apiVersion);}@Overridepublic 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;}@Overridepublic 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;}
}

定义一个controller测试

@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();}
}

定义User

@Builder
@Data
public class User {private Integer age;private String name;
}

测试

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/772284.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【LeetCode】283. 移动零(Java自用版)

双指针 class Solution {public void moveZeroes(int[] nums) {int n nums.length;int right 0, left 0;while (right < n) {if (nums[right] ! 0) {swap(nums, left, right);left;}right;}}public void swap (int[] nums, int left, int right) {int temp nums[left];…

一文700字从0到1教你实现Jmeter分布式压力测试!

之前写过用jmeter做接口测试的文章&#xff0c;本篇我们继续介绍下用jmeter做分布式压力测试的例子。 用jmeter做压力测试&#xff0c;如果只用一台机器&#xff0c;有鉴于线程数的限制和一台机器的性能&#xff0c;可能无法满足压力测试的实际需求&#xff0c;解决这个问题&a…

力扣--并查集1631.最小体力消耗路径

这题将图论和并查集联系起来。把数组每个位置看成图中的一个节点。 这段代码的主要思路是&#xff1a; 遍历地图中的每个节点&#xff0c;将每个节点与其相邻的下方节点和右方节点之间的边加入到边集合中&#xff08;因为从上到下和从下到上他们高度绝对值一样的&#xff0c;…

OpenHarmony之媒体组件模块简介

源码 本文基于OpenAtom OpenHarmony&#xff08;以下简称“OpenHarmony”&#xff09;3.2 Release源码foundation目录下的player_framework&#xff0c;在OpenHarmony 2.0 Release版本当中&#xff0c;这个模块的名字叫媒体组件模块&#xff0c;为了方便理解我们在本文中仍旧延…

VR全景展示:传统制造业如何保持竞争优势?

在结束不久的两会上&#xff0c;数字化经济和创新技术再度成为了热门话题。我国制造产业链完备&#xff0c;但是目前依旧面临着市场需求不足、成本传导压力加大等因素影响&#xff0c;那么传统制造业该如何保持竞争优势呢&#xff1f; 在制造行业中&#xff0c;VR全景展示的应用…

Query2doc——Query改写

大模型LLM最近一年比较火&#xff0c;但是可能由于数据量较大&#xff0c;存在一些矛盾的数据或者质量差的数据&#xff0c;就会导致大模型存在幻视情况&#xff0c;即存在严重不符合事实的情况。随着之而来&#xff0c;RAG&#xff08;Retrieval Augmented Generation&#xf…

计算机组成原理(超详解!!) 第三节 运算器(浮点加减乘)

1.浮点加法、减法运算 操作过程 1.操作数检查 如果能够判断有一个操作数为0&#xff0c;则没必要再进行后续一系列操作&#xff0c;以节省运算时间。 2.完成浮点加减运算的操作 (1) 比较阶码大小并完成对阶 使二数阶码相同&#xff08;即小数点位置对齐&#xff09;…

windows@浏览器主页被篡改劫持@360篡改主页@广告和弹窗设置@极速版

文章目录 360篡改浏览器主页方法1锁定浏览器主页 方法2注册表修改 360广告和弹窗360极速版 小结 360篡改浏览器主页 如果您使用360,且不想卸载它,那么当你启动360后,它可能会篡改你的浏览器(比如edge)的主页start page为360早期可能是通过修改快捷方式的target等属性,但是现在…

《剑指 Offer》专项突破版 - 面试题 93 : 最长斐波那契数列(C++ 实现)

题目链接&#xff1a;最长斐波那契数列 题目&#xff1a; 输入一个没有重复数字的单调递增的数组&#xff0c;数组中至少有 3 个数字&#xff0c;请问数组中最长的斐波那契数列的长度是多少&#xff1f;例如&#xff0c;如果输入的数组是 [1, 2, 3, 4, 5, 6, 7, 8]&#xff0…

C++模版(基础)

目录 C泛型编程思想 C模版 模版介绍 模版使用 函数模版 函数模版基础语法 函数模版原理 函数模版实例化 模版参数匹配规则 类模版 类模版基础语法 C泛型编程思想 泛型编程&#xff1a;编写与类型无关的通用代码&#xff0c;是代码复用的一种手段。 模板是泛型编程…

【前端Vue】Vue3+Pinia小兔鲜电商项目第3篇:静态结构搭建和分类实现,1. 整体结构创建【附代码文档】

Vue3ElementPlusPinia开发小兔鲜电商项目完整教程&#xff08;附代码资料&#xff09;主要内容讲述&#xff1a;认识Vue3&#xff0c;使用create-vue搭建Vue3项目1. Vue3组合式API体验,2. Vue3更多的优势,1. 认识create-vue,2. 使用create-vue创建项目,1. setup选项的写法和执行…

【OpenStack】OpenStack实战之开篇

目录 那么,OpenStack是什么?云又是什么?关于容器应用程序OpenStack如何适配其中?如何设置它?如何学会使用它?推荐超级课程: Docker快速入门到精通Kubernetes入门到大师通关课AWS云服务快速入门实战我的整个职业生涯到目前为止一直围绕着为离线或隔离网络设计和开发应用程…

【数据结构与算法】java有向带权图最短路径算法-Dijkstra算法(通俗易懂)

目录 一、什么是Dijkstra算法二、算法基本步骤三、java代码四、拓展&#xff08;无向图的Dijkstra算法&#xff09; 一、什么是Dijkstra算法 Dijkstra算法的核心思想是通过逐步逼近的方式&#xff0c;找出从起点到图中其他所有节点的最短路径。算法的基本步骤如下&#xff1a;…

应用层协议 - HTTP

文章目录 目录 文章目录 前言 1 . 应用层概要 2. WWW 2.1 互联网的蓬勃发展 2.2 WWW基本概念 2.3 URI 3 . HTTP 3.1 工作过程 3.2 HTTP协议格式 3.3 HTTP请求 3.3.1 URL基本格式 3.3.2 认识方法 get方法 post方法 其他方法 3.3.2 认识请求报头 3.3.3 认识请…

MyBatis是纸老虎吗?(七)

在上篇文章中&#xff0c;我们对照手动编写jdbc的开发流程&#xff0c;对MyBatis进行了梳理。通过这次梳理我们发现了一些之前文章中从未见过的新知识&#xff0c;譬如BoundSql等。本节我想继续MyBatis这个主题&#xff0c;并探索一下MyBatis中的缓存机制。在正式开始梳理前&am…

linux内核网络分析 用户空间工具 “每日读书”

有多种不同的工具可以用于配置linux众多可用的网络功能&#xff0c;如本章开头所述&#xff0c;你可以通过使用这些工具对内核巧妙的处理&#xff0c;以便于学习以及发现做这样的修改后的影响。 下面是在本书中将会经常涉及的工具&#xff1a; iputils 除了经常使用的ping命令外…

如何解决kafka rebalance导致的暂时性不能消费数据问题

文章目录 背景思考答案排它故障转移共享 背景 之前在review同组其它业务的时候&#xff0c;发现竟然把kafka去掉了&#xff0c;问了下原因&#xff0c;有一个单独的服务&#xff0c;我们可以把它称为agent&#xff0c;就是这个服务是动态扩缩容的&#xff0c;会采集一些指标&a…

Windows CMD命令大全(快速上手)

基础概念 在windows操作系统里进的DOS(即输入 winr CMD 进命令提示符)不是纯DOS,只是为方便某些需求而建立的, 而纯DOS本身就是一种操作系统.(两者的区别:比如你可以在纯DOS下删除你的 windows系统, 但在你所说的"命令提示符"里却不能,因为你不可能"在房子里面…

探索 Linux 系统信息工具:uname

在 Linux 系统中&#xff0c;uname 是一个非常实用的命令行工具&#xff0c;用于获取和打印系统特定的基本信息。本文将通过展示 uname 的用法及输出示例&#xff0c;帮助你更好地理解和掌握这一工具。 uname 命令简介 uname 工具提供了多种选项来显示不同的系统信息。当你在…

使用C++实现一个简单的日志功能

日志对于一些大一些的项目来说&#xff0c;可以在项目运行出现问题时更好的帮助 项目的维护人员快速的定位到问题出现的地方并且知道出现问题的原因&#xff0c; 并且日志也可以帮助程序员很好的进行项目的Debug&#xff0c;那么今天我就来实 现一个C编写的一个简单的日志功能。…