springboot如何通过注解优雅实现接口多版本管理
背景
一个web服务一旦上线后接口往往对接很多下游,下游应用可能强依赖这个接口,因此如果对接口进行版本更新最好是要兼容过去版本,提供一个相同功能的新版本。
接口版本管理的方式
- 通过请求参数实现
yunfei.group/user?version=v1 表示 v1版本的接口, 保持原有接口不动
yunfei.group/user?version=v2 表示 v2版本的接口,更新新的接口
- 通过域名实现(新版本接口在新实例,通过域名路由过去)
v1.yunfei.group/user
v2.yunfei.group/user
- 注解实现,通过重定义Handler Mapping过程实现。
注解方式实现接口版本管理
要实现的效果:
- 当没有匹配上版本时走默认接口
- 当有完全对应的版本时走完全匹配的接口
- 当没有完全对应的版本时走对应的最新版本
版本的定义:
- 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