SpringBoot 添加 url 版本控制注解 ApiVersion
作为服务端研发,在设计一个接口的时候,为了保证后续业务的新增和修改,会有各种的扩展性设计,其中关于 URL 的通用扩展设计是在 path 中加入版本。
例如: https://domain.com/api/{version}/path
。这种设计方便后续对接口升级时,只需要简单的升级版本号就可以了。
在有多个版本号之后,我们如何在 Spring Boot 中优雅的根据版本号进行不同的逻辑处理呢,本篇文章会介绍我正在使用的一种比较优雅的方法在 Spring
Boot 中来处理版本号。
准备工作
分析现在的状况
- 已有一个 url 并提供给了客户端进行使用,
https://www.domain.com/api/{version}/user/9527
- 现有的客户端提出一个不兼容的需求,但这个需求也是后续的通用需求
- 这些改动不能影响已发布的接口,保持对外承认
- 实现方式对现有代码侵入性小,且后续还能继续扩展版本
思路
- 给出的 url 已经预留了 version,我们可以基于这个字段进行扩展,讲 url 分为 1.0, 2.0, 3.0, 4.0 这样
- 使用自定义 ApiVersion 注解,编写成如下代码
1
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
public class UserController {
public UserVo detail( { userId)
return new UserVo();
}
public UserVo detailV1( { userId)
return new UserVo();
}
public UserVo detailV2( { userId)
return new UserVo();
}
public UserVo detailV3( { userId)
return new UserVo();
}
public UserVo detailV4( { userId)
return new UserVo();
}
} - ApiVersion 注解的作用:在没有的时候 version 走模糊匹配;有 ApiVersion 将走路径的准确匹配,将 {version} 替换为注解的 value。
调研
如何达到这个效果呢?
方案一:Spring 在进行路径匹配时,/api/1.0/user
的优先级比 /api/{version}/user
高。只要我们将定义 ApiVersion 的 RequestMapping 变化一下,替换 {version}
变成全路径。
方案二:在原来的路径匹配的方法后面再加个尾巴,如果有 ApiVersion 就进行第二次判断,判断解析的 PathVariable
version 的值是否和注解中定义的 value 一致。
查找资料
方案一
通过搜索引擎,查看代码,打断点 debug 等方法。在如下代码中有定义出 RequestMappingHandlerMapping
,这个类主要进行路径定义的匹配1
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
42public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
public RequestMappingHandlerMapping requestMappingHandlerMapping(
ContentNegotiationManager contentNegotiationManager,
FormattingConversionService conversionService,
{ ResourceUrlProvider resourceUrlProvider)
RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping();
mapping.setOrder(0);
mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
mapping.setContentNegotiationManager(contentNegotiationManager);
mapping.setCorsConfigurations(getCorsConfigurations());
PathMatchConfigurer pathConfig = getPathMatchConfigurer();
if (pathConfig.getPatternParser() != null) {
mapping.setPatternParser(pathConfig.getPatternParser());
}
else {
mapping.setUrlPathHelper(pathConfig.getUrlPathHelperOrDefault());
mapping.setPathMatcher(pathConfig.getPathMatcherOrDefault());
Boolean useSuffixPatternMatch = pathConfig.isUseSuffixPatternMatch();
if (useSuffixPatternMatch != null) {
mapping.setUseSuffixPatternMatch(useSuffixPatternMatch);
}
Boolean useRegisteredSuffixPatternMatch = pathConfig.isUseRegisteredSuffixPatternMatch();
if (useRegisteredSuffixPatternMatch != null) {
mapping.setUseRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch);
}
}
Boolean useTrailingSlashMatch = pathConfig.isUseTrailingSlashMatch();
if (useTrailingSlashMatch != null) {
mapping.setUseTrailingSlashMatch(useTrailingSlashMatch);
}
if (pathConfig.getPathPrefixes() != null) {
mapping.setPathPrefixes(pathConfig.getPathPrefixes());
}
return mapping;
}
}
RequestMappingHandlerMapping
中有个很重要的方法 match
,这是运行时进行 Request 匹配的方法,匹配到了就执行相应的类和方法。1
2
3
4
5
6
7
8
9
10
11
public RequestMatchResult match(HttpServletRequest request, String pattern) {
Assert.isNull(getPatternParser(), "This HandlerMapping requires a PathPattern");
RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(this.config).build();
RequestMappingInfo match = info.getMatchingCondition(request);
return (match != null && match.getPatternsCondition() != null ?
new RequestMatchResult(
match.getPatternsCondition().getPatterns().iterator().next(),
UrlPathHelper.getResolvedLookupPath(request),
getPathMatcher()) : null);
}
而在这个方法里面主要是调用了 RequestMappingInfo
的 getMatchingCondition
方法,方法如下。
作用是按 POST GET
,参数,header 等顺序进行匹配,其中 PatternsRequestCondition
和 PathPatternsRequestCondition
是我们研究的重点。PathPatternsRequestCondition
是最新的 path 解析工具 PatternsRequestCondition
是老版解析,正在被废弃中。
所以我们就围绕这个 RequestMappingInfo
来进行。1
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
42public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
if (methods == null) {
return null;
}
ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
if (params == null) {
return null;
}
HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
if (headers == null) {
return null;
}
ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
if (consumes == null) {
return null;
}
ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
if (produces == null) {
return null;
}
PathPatternsRequestCondition pathPatterns = null;
if (this.pathPatternsCondition != null) {
pathPatterns = this.pathPatternsCondition.getMatchingCondition(request);
if (pathPatterns == null) {
return null;
}
}
PatternsRequestCondition patterns = null;
if (this.patternsCondition != null) {
patterns = this.patternsCondition.getMatchingCondition(request);
if (patterns == null) {
return null;
}
}
RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
if (custom == null) {
return null;
}
return new RequestMappingInfo(this.name, pathPatterns, patterns,
methods, params, headers, consumes, produces, custom, this.options);
}
RequestMappingHandlerMapping
这个类有 2 个方法是预留给我们进行自定义的。我们在两个方法中创建自己的 Condition。1
2
3
4
5
6
7
8
9protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
return null;
}
protected RequestCondition<?> getCustomMethodCondition(Method method) {
return null;
}
如果选用方案一,我们就可以直接创建一个 PatternsRequestCondition
或者 PathPatternsRequestCondition
,这样我们全路径的 Condition 优先级将更高,那么将悠闲访问带版本号的 RequestMapping,我们的目的就达到了。
代码如下1
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
private ApiVersionProperties apiVersionProperties;
private ConditionFactory conditionFactory;
public ApiVersionRequestMappingHandlerMapping(ApiVersionProperties apiVersionProperties, ConditionFactory conditionFactory) {
this.apiVersionProperties = apiVersionProperties;
this.conditionFactory = conditionFactory;
}
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
ApiVersions apiVersions = AnnotationUtils.findAnnotation(handlerType, ApiVersions.class);
ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
if (apiVersions == null && apiVersion == null) {
return super.getCustomTypeCondition(handlerType);
}
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(handlerType, RequestMapping.class);
if (apiVersions != null) {
return createRequestCondition(requestMapping, null, apiVersions);
}
return createRequestCondition(requestMapping, null, apiVersion);
}
protected RequestCondition<?> getCustomMethodCondition(Method method) {
Class<?> methodClass = method.getDeclaringClass();
ApiVersions apiVersions = AnnotationUtils.findAnnotation(method, ApiVersions.class);
ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
if (apiVersions == null && apiVersion == null) {
return super.getCustomMethodCondition(method);
}
RequestMapping classMapping = AnnotatedElementUtils.findMergedAnnotation(methodClass, RequestMapping.class);
RequestMapping methodMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
if (apiVersions != null) {
return createRequestCondition(classMapping, methodMapping, apiVersions);
}
return createRequestCondition(classMapping, methodMapping, apiVersion);
}
protected RequestCondition<?> createRequestCondition(RequestMapping classMapping, RequestMapping methodMapping, ApiVersions apiVersions) {
return createRequestCondition(classMapping, methodMapping, apiVersions.value());
}
protected RequestCondition<?> createRequestCondition(RequestMapping classMapping, RequestMapping methodMapping, ApiVersion... apiVersions) {
if (apiVersions == null || apiVersions.length < 1) {
return null;
}
List<String> paths = null;
if (classMapping == null && methodMapping == null) {
return null;
} else if (methodMapping == null) {
String[] classPaths = classMapping.value();
paths = Stream.of(classPaths).collect(Collectors.toList());
} else if (classMapping == null) {
String[] methodPaths = methodMapping.value();
paths = Stream.of(methodPaths).collect(Collectors.toList());
} else {
String[] classPaths = classMapping.value();
String[] methodPaths = methodMapping.value();
paths = Arrays.stream(classPaths).flatMap(classPath -> Arrays.stream(methodPaths).map(methodPath -> classPath + methodPath)).collect(Collectors.toList());
}
List<String> apiVersionValues = Arrays.stream(apiVersions).map(ApiVersion::value).collect(Collectors.toList());
String[] fullPaths = paths.stream().flatMap(path -> apiVersionValues.stream().map(apiVersion -> this.replacePlaceholder(path, apiVersion))).toArray(String[]::new);
/* return new PatternsRequestCondition(fullPaths);*/
return conditionFactory.create(fullPaths);
}
protected String replacePlaceholder(String text, String apiVersion) {
if (ObjectUtils.isEmpty(text)) {
return text;
}
int startIndex = text.indexOf(apiVersionProperties.getQuoteLeft());
if (startIndex == -1) {
return text;
}
int endIndex = text.indexOf(apiVersionProperties.getQuoteRight());
if (endIndex == -1) {
return text;
}
String startString = text.substring(0, startIndex);
String endString = text.substring(endIndex + 1);
return startString + apiVersion + endString;
}
}
配置类中进行如下配置
本配置的作用是在低版本是没有 PathPatternsRequestCondition
, 兼容旧版本1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public PathPatternsRequestConditionFactory pathPatternsRequestCondition() {
return new PathPatternsRequestConditionFactory();
}
public PatternsRequestConditionFactory patternsRequestConditionFactory() {
return new PatternsRequestConditionFactory();
}
public WebMvcRegistrations webMvcRegistrations(ApiVersionProperties apiVersionProperties, ConditionFactory conditionFactory) {
return new WebMvcRegistrations() {
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new ApiVersionRequestMappingHandlerMapping(apiVersionProperties, conditionFactory);
}
};
}
接下来在 Controller 中我们就可以如下使用了1
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
public class VersionController {
public String v1() {
return "v1";
}
public String v11() {
return "v11";
}
public String v2() {
return "v2";
}
public String v3() {
throw new RuntimeException("12312312");
}
}
方案二
暂时还没有好的思路,如果看的同学有较好的思路,可以在后面留言回复下。