接口不可能一成不变,需要根据业务需求不断增加内部逻辑。如果做大的功能调整或重构, 涉及参数定义的变化或是参数废弃,导致接口无法向前兼容,这时接口就需要有版本的概念。在考虑接口版本策略设计时,我们需要注意的是,最好一开始就明确版本策略,并考虑在整个服务端统一版本策略。
第一,版本策略最好一开始就考虑。
既然接口总是要变迁的,那么最好一开始就确定版本策略。比如,确定是通过 URL Path 实现,是通过 QueryString 实现,还是通过 HTTP 头实现。这三种实现方式的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @GetMapping("/v1/api/user") public int right1(){ return 1; }
@GetMapping(value = "/api/user", params = "version=2") public int right2(@RequestParam("version") int version) { return 2; }
@GetMapping(value = "/api/user", headers = "X-API-VERSION=3") public int right3(@RequestHeader("X-API-VERSION") int version) { return 3; }
|
这样,客户端就可以在配置中处理相关版本控制的参数,有可能实现版本的动态切换。
这三种方式中,URL Path 的方式最直观也最不容易出错;QueryString 不易携带,不太推荐作为公开 API 的版本策略;HTTP 头的方式比较没有侵入性,如果仅仅是部分接口需要进行版本控制,可以考虑这种方式。
第二,版本实现方式要统一。
之前,遇到过一个 O2O 项目,需要针对商品、商店和用户实现 REST 接口。虽然大家约定通过 URL Path 方式实现 API 版本控制,但实现方式不统一,有的是 /api/item/v1
, 有的是 /api/v1/shop
,还有的是 /v1/api/merchant
:
1 2 3 4 5 6 7 8 9
| @GetMapping("/api/item/v1") public void wrong1(){ } @GetMapping("/api/v1/shop") public void wrong2(){ } @GetMapping("/v1/api/merchant") public void wrong3(){ }
|
显然,商品、商店和商户的接口开发同学,没有按照一致的 URL 格式来实现接口的版本控制。更要命的是,我们可能开发出两个 URL 类似接口,比如一个是 /api/v1/user
,另一个 是 /api/user/v1
,这到底是一个接口还是两个接口呢?
相比于在每一个接口的 URL Path 中设置版本号,更理想的方式是在框架层面实现统一。如果你使用 Spring 框架的话,可以按照下面的方式自定义 RequestMappingHandlerMapping 来实现。
首先,创建一个注解来定义接口的版本。@APIVersion 自定义注解可以应用于方法或 Controller 上:
1 2 3 4 5
| @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface APIVersion { String[] value(); }
|
然后,定义一个 APIVersionHandlerMapping 类继承 RequestMappingHandlerMapping。
RequestMappingHandlerMapping 的作用,是根据类或方法上的 @RequestMapping 来生成 RequestMappingInfo 的实例。我们覆盖 registerHandlerMethod 方法的实现, 从 @APIVersion 自定义注解中读取版本信息,拼接上原有的、不带版本号的 URL Pattern,构成新的 RequestMappingInfo,来通过注解的方式为接口增加基于 URL 的版本号:
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
| public class APIVersionHandlerMapping extends RequestMappingHandlerMapping {
@Override protected boolean isHandler(Class<?> beanType) { return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class); }
@Override protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) { Class<?> controllerClass = method.getDeclaringClass(); APIVersion apiVersion = AnnotationUtils.findAnnotation(controllerClass, APIVersion.class); APIVersion methodAnnotation = AnnotationUtils.findAnnotation(method, APIVersion.class); if (Objects.nonNull(methodAnnotation)) { apiVersion = methodAnnotation; } String[] urlPatterns = Objects.isNull(apiVersion) ? new String[0] : apiVersion.value(); PatternsRequestCondition apiPattern = new PatternsRequestCondition(urlPatterns); PatternsRequestCondition oldPattern = mapping.getPatternsCondition(); PatternsRequestCondition updatedFinalPattern = apiPattern.combine(oldPattern); mapping = new RequestMappingInfo(mapping.getName(), updatedFinalPattern, mapping.getMethodsCondition(), mapping.getParamsCondition(), mapping.getHeadersCondition(), mapping.getConsumesCondition(), mapping.getProducesCondition(), mapping.getCustomCondition()); super.registerHandlerMethod(handler, method, mapping); } }
|
最后,也是特别容易忽略的一点,要通过实现 WebMvcRegistrations 接口,来生效自定义的 APIVersionHandlerMapping:
1 2 3 4 5 6 7 8
| @Configuration public class CommonMistakesApplication implements WebMvcRegistrations {
@Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { return new APIVersionHandlerMapping(); } }
|
这样,就实现了在 Controller 上或接口方法上通过注解,来实现以统一的 Pattern 进行版本号控制:
1 2 3 4 5
| @GetMapping(value = "/api/user") @APIVersion("v4") public int right4() { return 4; }
|
使用框架来明确 API 版本的指定策略,不仅实现了标准化,更实现了强制的 API 版本控制。对上面代码略做修改,我们就可以实现不设置 @APIVersion 接口就给予报错提示。