用 @ApiVersion 注解给 API 增加版本号
通常我们设计 REST 接口时会要求在 API 上增加版本号,以方便在接口升级时保证一定的兼容性。我们假定设计的接口入下:
1 | GET /api/v1/user/{userId} |
在 Spring MVC 中可以这样这样实现:
1 |
|
在 GetMapping
注解中手动指定了接口版本,但每个接口都指定或指定在类上也是非常不方便的,我们希望有一个注解来简化的约束这个配置的过程
需求
- 实现类上注解,统一对
Controller
下的@RequestMapping
生效 - 方法上注解覆盖类上注解,方便后续升级版本
实现
先定义版本注解
1 |
|
该注解定义里,value 表示接口的版本,默认我们定义为 v1
;prefix 为接口前缀,默认是空。
同时,@Mapping
元注解表示该注解类是 Web Mapping 注解。@RequestMapping
注解配置了使用这个 @ApiVersion
注解时生效的 Mapping 配置,
这里将接口的响应内容格式设定为 application/json;charset=utf-8
,因为我们这个约定 API 接口的返回应当是 JSON 格式的。
开发 RequestCondition
RequestCondition
RequestCondition
是 Spring 中用于描述请求映射条件的。当请求进来时,会通过调用 RequestCondition
的 getMatchingCondition
方法,
验证该方法是否匹配该处理方法。
从请求进入到查询匹配,大概调用链是这样的:
DispatcherServlet.getHandler
-> HandlerMapping(RequestMappingInfoHandlerMapping).getHandlerInternal
-> AbstractHandlerMethodMapping.lookupHandlerMethod
-> RequestMappingInfo.getMatchingCondition
-> RequestCondition.getMatchingCondition
ApiVersionCondition
该类有三个方法需要实现,第一个是 combine 方法。
1 | T combine(T other); |
该方法是两个 RequestCondition
对象合并时调用,比如同时存在类级和方法级的 @RequestMapping
注解。Lhs 是类级别,rhs 是方法级别。
在 ApiVersionCondition
中我们需求是忽略类上的,只保留方法上的配置,所以代码应该是
1 |
|
下一个方法就是前面介绍过的方法,用于判断请求是否匹配,匹配则返回 this,不匹配则返回 null
1 |
|
由于我们的 @ApiVersion
只修改 URI 的前缀,因此我们主要取出合适的 API 路径作一下比较既可:
1 |
|
我们 API 的路径实际是这样的:
1 | https://domain/contextPath/prefix/path |
contextPath
部分应该是要去掉的,所以我们 getMatchingCondition 方法将其去除后再进行前缀判断。
最后一个要实现的方法是 compareTo。该方法用于和其它的 ApiVersionCondition
进行比较,以选取最匹配的。
1 |
|
比较方法原则:
在比较方法中,this 相当于 lhs,other 相当于 rhs。
- 当 lhs < rhs 时返回
负值
(lhs - rhs < 0);- 当 lhs > rhs 时返回
正值
(lhs - rhs > 0);- 相等返回 0(lhs - rhs = 0)
排序的 Natural Ordering:
排序是按照自然排序来排的,也就是从低到高,从小到大
以下代码是 Mapping 查找方法 lookupHandlerMethod
选取最佳匹配的方式,先用 sort 排序,再选第一个匹配的。
1 | // org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod |
按照比较原则,两个 Condition 比较(this 和 other),哪个对象能匹配上,哪个对象就优先(小的优先)。如 other.getMatchingCondition() 返回非 NULL 成功匹配上,所以让其优先。
既 this > other,返回正值。
第二行 this.getMatchingCondition(request) != null
表示 other 的没匹配上,this 的匹配上了,使 this < other,所以返回负值。
最后如果谁都没匹配上,就返回 0。
以上的说明更多的是说明 Comparable
接口和 Arrays.sort 方法的约定逻辑。Sort 方法基于自然排序,将实现了 Comparable
接口的对象按从小到大排序。
扩展 Spring RMHM
默认的 RequestMappingHandlerMapping
类并不能支持我们的自定义 Condition
和自定义注解 @ApiVersion
,所以我们还需对 RequestMappingHandlerMapping
做些扩展。
要实现我们需要的功能,只需要扩展以下三个方法,这三个方法也是实现自定义注解处理的常用模板方法。
1 | public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping { |
getCustomTypeCondition
方法用于取得应用在类上的 Condition,一般是注解在类上的 @Mapping 注解,这里我们取出注解在类上的 ApiVersion
注解信息,构造一个 ApiVersionCondition
对象。
1 |
|
getCustomMethodCondition
方法和 getCustomTypeCondition
相同,只是该方法用于取出应用在方法上的 Condition。
1 |
|
getMappingForMethod
方法是实际上是组合以上两个方法的返回的 Condition,合并产生一个 Condition。所以在处理逻辑上还会调用上面的两个方法来获取 Condition。
1 |
|
上面代码先从 method 取(先调用 getCustomMethodCondition)的原因是我们要求方法上的注解配置优先级比类上高。如果方法上已经注解了,就忽略类上的注解配置。这点和 @RequestMapping
是不一样的,@RequestMapping
会将类上的配置和方法上的配置拼接起来(逻辑在 super.getMappingForMethod
中),显然我们不需要这样做。
最后一行,我们基于 @ApiVersion
构造的 RequestMappingInfo
被当成 lhs,而 Spring 产生的是 rhs。这是因为 Spring 产生的是真正在 Controller 里配置的 @RequestMapping
接口信息,而我们的是 @ApiVersion
配置的信息。最终的接口地址应该是这样的形式:
1 | [prefix][version][RequestMapping] |
[prefix][version]
是 @ApiVersion
配置的,所以作为 lhs 才能实现我们的要求。
注册到 Spring
开发完 ApiVersionRequestMappingHandlerMapping
后,基本功能已经完成,但是要使之生效还需向 Spring 注册它。可以通过实现 WebMvcRegistrations
接口的 getRequestMappingHandlerMapping
方法,配置默认的RequestMappingHandlerMapping
为 ApiVersionRequestMappingHandlerMapping
,这样 Spring Boot 自动配置会自动完成配置。
1 | public class ApiVersionRegistration implements WebMvcRegistrations { |
总结
Spring 提供了很多扩展点帮助我们很轻松实现我们需要的特殊功能,后面有空好好梳理一下 Spring 的各个阶段的流程和实现,也许可以对 Spring 有更深入的理解。
用 @ApiVersion 注解给 API 增加版本号
https://blog.imoe.tech/2020/05/20/32-use-annotation-for-api-version/