Spring Boot版本:2.3.4.RELEASE
目的
项目迭代升级,接口要更新,并且要在接口路径不变的清空下兼容老接口,就可以做接口版本管理,像这样:
老接口:
{
'headers': {
'apiverison': 'v1',
'apiplatform': 'web',
...
},
'url': '/user/login',
'data': {
'username': 'cc',
'password': '123'
},
...
}
复制代码
新接口:
{
'headers': {
'apiverison': 'v2',
'apiplatform': 'web',
...
},
'url': '/user/login',
'data': {
'username': 'cc',
'password': '123'
},
...
}
复制代码
对于前端来说,可以在headers中指定接口的版本,不需要修改接口的路径。
除了指定接口版本,还能指定接口的支持平台,比如web端、移动端。
实现
需要四个实现类,和web mvc的配置类
目录是这样的:
- com.cc
- config
- version
ApiHandlerMapping
ApiPlatform
ApiVersion
ApiVersionCondition
WebMvcConfig
复制代码
@ApiVersion:
package com.cc.config.version;
import org.springframework.lang.Nullable;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD}) // 注解修饰的对象范围,TYPE:类,METHOD:方法
@Retention(RetentionPolicy.RUNTIME) // 注解保存到class和jvm内
public @interface ApiVersion {
// 标识版本号
int value();
// 兼容平台
int platform() default 0;
}
复制代码
ApiPlatform:
package com.cc.config.version;
/**
* api接口调用平台声明
* @author cc
* @date 2021-11-19 11:23
*/
public interface ApiPlatform {
/**
* 默认
*/
public static int DEFAULT = 0;
/**
* web端
*/
public static int WEB = 1;
/**
* 移动端
*/
public static int MOBILE = 2;
}
复制代码
ApiHandlerMapping:
package com.cc.config.version;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.lang.reflect.Method;
public class ApiHandlerMapping extends RequestMappingHandlerMapping {
// 对类修饰的注解
@Override
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
// 判断是否有@ApiVersion注解
ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
return createCondition(apiVersion);
}
// 对方法修饰的注解
@Override
protected RequestCondition<?> getCustomMethodCondition(Method method) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
return createCondition(apiVersion);
}
// 创建基于@APIVersion的RequestCondition
private RequestCondition<ApiVersionCondition> createCondition(ApiVersion apiVersion) {
return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value());
}
}
复制代码
ApiVersionCondition:
package com.cc.config.version;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import javax.servlet.http.HttpServletRequest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
// 路径中版本的前缀,这里写正则:\d表示一位数字,\d+表示一位以上数字,如v1,v2
private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+)");
// header中的key
private final static String HEADER_VERSION = "apiversion";
private final static String HEADER_PLATFORM = "apiplatform";
// api的版本
private int apiVersion;
// api的平台
private int apiplatform;
public ApiVersionCondition(int apiVersion, int apiplatform) {
this.apiVersion = apiVersion;
this.apiplatform = apiplatform;
}
// 将不同的筛选条件合并
@Override
public ApiVersionCondition combine(ApiVersionCondition apiVersionCondition) {
// 采用最后定义优先原则,则方法上的定义覆盖类上面的定义
return new ApiVersionCondition(apiVersionCondition.getApiVersion(), apiVersionCondition.getApiplatform());
}
// 根据request查找匹配到的筛选条件
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
/**
* 正则匹配请求header参数中是否有版本号
* 版本号应该以v开头,如:v1、v2,这个可以通过修改正则自定义
*/
String apiversion = request.getHeader(HEADER_VERSION);
String platformStr = request.getHeader(HEADER_PLATFORM);
int apiplatform = 0;
if (!StringUtils.isEmpty(platformStr)) {
apiplatform = Integer.parseInt(platformStr);
}
if (!StringUtils.isEmpty(apiversion)) {
Matcher m = VERSION_PREFIX_PATTERN.matcher(apiversion);
if (m.find()) {
int version = Integer.parseInt(m.group(1));
// 版本匹配到了
if (version == this.apiVersion) {
// 如果有传入平台platform参数,那么就找指定了平台的接口,找不到该接口就不通过
if (apiplatform > 0) {
if (this.apiplatform == apiplatform) {
return this;
} else {
return null;
}
}
// 如果该接口指定了平台,但是没有传platform或者传错,那么也不允许通过
if (this.apiplatform > 0 && this.apiplatform != apiplatform) {
return null;
}
return this;
}
// 没有可以匹配的接口
return null;
}
}
throw new RuntimeException("请检查header中的版本参数");
}
// 不同筛选条件比较,用于排序
@Override
public int compareTo(ApiVersionCondition apiVersionCondition, HttpServletRequest httpServletRequest) {
return apiVersionCondition.getApiVersion() - this.apiVersion;
}
public int getApiVersion() {
return apiVersion;
}
public void setApiVersion(int apiVersion) {
this.apiVersion = apiVersion;
}
public int getApiplatform() {
return apiplatform;
}
public void setApiplatform(int apiplatform) {
this.apiplatform = apiplatform;
}
}
复制代码
WebMvcConfig:
package com.cc.config;
import com.cc.config.version.ApiHandlerMapping;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.resource.ResourceUrlProvider;
/**
* web mvc的配置类
* @author cc
* @date 2021-07-12 10:29
*/
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
// 版本管理相关
@Override
public RequestMappingHandlerMapping requestMappingHandlerMapping(ContentNegotiationManager contentNegotiationManager, FormattingConversionService conversionService, ResourceUrlProvider resourceUrlProvider) {
RequestMappingHandlerMapping handlerMapping = new ApiHandlerMapping();
handlerMapping.setOrder(0);
return handlerMapping;
}
}
复制代码
使用
后端编写接口的时候只需要添加@ApiVersion注解即可:
package com.cc.controller;
import com.cc.config.version.ApiPlatform;
import com.cc.config.version.ApiVersion;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@ApiVersion(1)
@GetMapping("/login")
public String login() {
return "this api version is v1";
}
@ApiVersion(2)
@GetMapping("/login")
public String login2() {
return "this api version is v2";
}
@ApiVersion(value = 2, platform = ApiPlatform.WEB)
@GetMapping("/login")
public String login21() {
return "this api version is v3";
}
// 要避免这种相同版本和平台的接口,这种请求的时候会报错,因为和上面的接口重复了,系统就不知道你要请求的是哪个
// @ApiVersion(value = 2, platform = ApiPlatform.WEB)
// @GetMapping("/login")
// public String login211() {
// return "this api version is v4";
// }
}
复制代码
controller里一共有三个接口,分别的调用方式是:
-
接口版本为v1,那么header是这样的:
{ 'headers': { 'apiverison': 'v1', ... }, 'url': '/login', ... } 复制代码
请求结果为:
this api version is v1
-
接口版本为v2,header是这样的:
{ 'headers': { 'apiverison': 'v2', ... }, 'url': '/login', ... } 复制代码
请求结果为:
this api version is v2
-
接口版本为v2,并且仅支持web端平台,在ApiPlatform里可以知道web端的标识是1,所以header是这样:
{ 'headers': { 'apiverison': 'v2', 'apiplatform': '1', ... }, 'url': '/login', ... } 复制代码
请求结果为:
this api version is v3
另外,@ApiVersion注解还能作用于类上,可以很方便的给类里所有的接口指定版本,并且因为最后定义优先原则,作用于函数上的注解会覆盖类上面的,所以像下面的代码:
package com.cc.controller;
import com.cc.config.version.ApiVersion;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@ApiVersion(1)
@RestController
public class TestController {
@GetMapping("/t")
public String t1() {
return "t1";
}
@ApiVersion(2)
@GetMapping("/t")
public String t2() {
return "t2";
}
}
复制代码
t1函数的接口版本为类声明的v1,t2函数的接口版本为覆盖后的v2。