SpringBoot - 接口版本
# SpringBoot - 接口版本
# 为什么接口需要版本?
接口不可能一成不变,需要根据业务需求不断增加内部逻辑。如果做大的功能调整或重构,涉及参数定义的变化或是参数废弃,导致接口无法向前兼容,这时接口就需要有版本的概念。在考虑接口版本策略设计时,我们需要注意的是,最好一开始就明确版本策略,并考虑在整个服务端统一版本策略。
# 实现接口版本的方式
通过URI进行版本控制
http://127.0.0.1:8095/api/v1/user
@GetMapping("/api/v1/user") public String getUser1(){ return "api version: v1"; }
1
2
3
4通过请求参数进行版本控制
http://127.0.0.1:8095/api/user?version=2
@GetMapping(value = "/api/user", params = "version=2") public String getUser2(@RequestParam("version") int version) { return "api version: v2"; }
1
2
3
4通过自定义Header进行版本控制
http://127.0.0.1:8095/api/user
,headers=[X-API-VERSION=1]
@GetMapping(value = "/api/user", headers = "X-API-VERSION=3") public String getUser3(@RequestHeader("X-API-VERSION") int version) { return "api version: v3"; }
1
2
3
4通过媒体类型进行版本控制
http://127.0.0.1:8095/api/user
,headers=[Accept=application/api-v1+json]
@GetMapping(value="/api/user", produces = "application/api-v4+json") public String getUser4() { return "api version: v4"; }
1
2
3
4
这4种方式中,第一种URI的方式最直观也最不容易出错;QueryString 不易携带,不太推荐作为公开 API 的版本策略;HTTP 头的方式比较没有侵入性,如果仅仅是部分接口需要进行版本控制,可以考虑这种方式。
**版本实现方式要统一。**接口通常由多人一起开发,如果版本实现方式不统一有可能开发出多个URL相似功能相同的接口,比如:v1/api/user
和 api/v1/user
等,使调用者产生疑问,这到底是一个接口还是两个接口呢?应该调用哪个?下面以第一种URI方式来实现统一的版本控制。
# 实现案例
相比于在每一个接口的 URL Path 中设置版本号,更理想的方式是在框架层面实现统一。如果使用 Spring 框架的话,可以按照下面的方式自定义 RequestMappingHandlerMapping 来实现。
# 自定义@ApiVersion注解
/**
* @author jason
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
String value();
}
2
3
4
5
6
7
8
9
10
# 定义版本匹配RequestCondition
版本匹配支持三层版本
- v1.1.1 (大版本.小版本.补丁版本)
- v1.1 (等同于v1.1.0)
- v1 (等同于v1.0.0)
/**
* @author jason
*/
@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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# 定义HandlerMapping
/**
* @author jason
*/
public class ApiVersionHandlerMapping 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());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 配置注册HandlerMapping
/**
* @author jason
*/
@Configuration
public class CustomWebMvcConfiguration extends WebMvcConfigurationSupport {
@Override
public RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
return new ApiVersionHandlerMapping();
}
}
2
3
4
5
6
7
8
9
10
# 测试运行
@ApiVersion("5")
@GetMapping(value="/api/{v}/user")
public String getUser5() {
return "api version: v5";
}
@ApiVersion("6.0.1")
@GetMapping(value="/api/{v}/user")
public String getUser6() {
return "api version: v6.0.1";
}
2
3
4
5
6
7
8
9
10
11
返回结果:
{
"code": 200,
"message": "SUCCESS",
"data": "api version: v5",
"timestamp": 1682128551795
}
{
"code": 200,
"message": "SUCCESS",
"data": "api version: v6.0.1",
"timestamp": 1682128651335
}
2
3
4
5
6
7
8
9
10
11
12
13
使用框架来明确 API 版本的指定策略,不仅实现了标准化,更实现了强制的 API 版本控制。对上面代码略做修改,我们就可以实现不设置 @APIVersion 接口就给予报错提示,或者限制最大版本、版本集合等。
# 示例源码
- https://github.com/hengwen/spring-demo/tree/main/springbootapiversion
# 参考
- https://pdai.tech/md/spring/springboot/springboot-x-interface-version.html