0%

接口设计-接口版本管理

接口不可能一成不变,需要根据业务需求不断增加内部逻辑。如果做大的功能调整或重构, 涉及参数定义的变化或是参数废弃,导致接口无法向前兼容,这时接口就需要有版本的概念。在考虑接口版本策略设计时,我们需要注意的是,最好一开始就明确版本策略,并考虑在整个服务端统一版本策略。

第一,版本策略最好一开始就考虑。

既然接口总是要变迁的,那么最好一开始就确定版本策略。比如,确定是通过 URL Path 实现,是通过 QueryString 实现,还是通过 HTTP 头实现。这三种实现方式的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//通过URL Path实现版本控制
@GetMapping("/v1/api/user")
public int right1(){
return 1;
}
//通过QueryString中的version参数实现版本控制
@GetMapping(value = "/api/user", params = "version=2")
public int right2(@RequestParam("version") int version) {
return 2;
}
//通过请求头中的X-API-VERSION参数实现版本控制
@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 {

/**
* 若是该类 beanType上存在 @Controller注解,则会走 registerHandlerMethod 方法
* @param beanType
* @return
*/
@Override
protected boolean isHandler(Class<?> beanType) {
return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
}

@Override
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
// 优先取方法上的 APIVersion 注解对象
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 接口就给予报错提示。

------ 本文结束------