实战:实现Web API版本控制
前面介绍了Spring Boot如何构建RESTful风格的Web应用接口以及使用Swagger生成API的接口文档。如果业务需求变更,Web API功能发生变化时应该如何处理呢?可以通过Web API的版本控制来处理。
1.为什么进行版本控制
一般来说,Web API是提供给其他系统或其他公司使用的,不能随意频繁地变更。然而,由于需求和业务不断变化,Web API也会随之不断修改。如果直接对原来的接口修改,势必会影响其他系统的正常运行。
例如,系统中用户添加的接口/api/user由于业务需求的变化,接口的字段属性也发生了变化,而且可能与之前的功能不兼容。为了保证原有的接口调用方不受影响,只能重新定义一个新的接口:/api/user2,这使得接口维护难度增加。
如何做到在不影响现有调用方的情况下,优雅地更新接口的功能呢?
最简单高效的办法就是对Web API进行有效的版本控制。通过增加版本号来区分对应的版本,来满足各个接口调用方的需求。版本号的使用有以下几种方式:
1)通过域名进行区分,即不同的版本使用不同的域名,如v1.api.test.com、v2.api.test.com。
2)通过请求URL路径进行区分,在同一个域名下使用不同的URL路径,如test.com/api/v1/、test.com/api/v2。
3)通过请求参数进行区分,在同一个URL路径下增加version=v1或v2等,然后根据不同的版本选择执行不同的方法。
在实际项目开发中,一般选择第二种方式,因为这样既能保证水平扩展,又不影响以前的老版本。
2.Web API的版本控制
Spring Boot对RESTful的支持非常全面,因而实现RESTful API非常简单,同样对于API版本控制也有相应的实现方案:
1)创建自定义的@APIVersion注解。
2)自定义URL匹配规则ApiVersionCondition。
3)使用RequestMappingHandlerMapping创建自定义的映射处理程序,根据Request参数匹配符合条件的处理程序。
下面通过示例程序来演示Web API如何增加版本号。
步骤01 创建自定义注解。
创建一个自定义版本号标记注解@ApiVersion。实现代码如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
/**
* @return版本号
*/
int value() default 1;
}
在上面的示例中,创建了ApiVersion自定义注解用于API版本控制,并返回了对应的版本号。
步骤02 自定义URL匹配逻辑。
接下来定义URL匹配逻辑,创建ApiVersionCondition类并继承RequestCondition
接口,其作用是进行版本号筛选,将提取请求URL中的版本号与注解上定义的版本号进行对比,以此来判断某个请求应落在哪个控制器上。实现代码如下:
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile(".*v(\\d).*");
private int apiVersion;
ApiVersionCondition(int apiVersion) {
this.apiVersion = apiVersion;
}
private int getApiVersion() {
return apiVersion;
}
@Override
public ApiVersionCondition combine(ApiVersionCondition apiVersionCondition) {
return new ApiVersionCondition(apiVersionCondition.getApiVersion());
}
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {
Matcher m = VERSION_PREFIX_PATTERN.matcher(httpServletRequest.getRequestURI());
if (m.find()) {
Integer version = Integer.valueOf(m.group(1));
if (version>=this.apiVersion) {
return this;
}
}
return null;
}
@Override
public int compareTo(ApiVersionCondition apiVersionCondition, HttpServletRequest httpServletRequest) {
return apiVersionCondition.getApiVersion() - this.apiVersion;
}
}
在上面的示例中,通过ApiVersionCondition类重写RequestCondition定义URL匹配逻辑。
当方法级别和类级别都有ApiVersion注解时,通过ApiVersionRequestCondition.combine
方法将二者进行合并。最终将提取请求URL中的版本号,与注解上定义的版本号进行对比,判断URL是否符合版本要求。
步骤03 自定义匹配的处理程序。
接下来实现自定义匹配的处理程序。先创建ApiRequestMappingHandlerMapping类,重写部分RequestMappingHandlerMapping的方法,实现自定义的匹配处理程序。示例代码如下:
public class ApiRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
private static final String VERSION_FLAG = "{version}";
private static RequestCondition<ApiVersionCondition> createCondition(Class<?> clazz) {
RequestMapping classRequestMapping = clazz.getAnnotation(RequestMapping.class);
if (classRequestMapping == null) {
return null;
}
StringBuilder mappingUrlBuilder = new StringBuilder();
if (classRequestMapping.value().length > 0) {
mappingUrlBuilder.append(classRequestMapping.value()[0]);
}
String mappingUrl = mappingUrlBuilder.toString();
if (!mappingUrl.contains(VERSION_FLAG)) {
return null;
}
ApiVersion apiVersion = clazz.getAnnotation(ApiVersion.class);
return apiVersion == null ? new ApiVersionCondition(1) : new ApiVersionCondition(apiVersion.value());
}
@Override
protected RequestCondition<?> getCustomMethodCondition(Method method) {
return createCondition(method.getClass());
}
@Override
protected RequestCondition<?> getCustomTypeCondition(Class<?> handerType) {
return createCondition(handerType);
}
}
步骤04 配置注册自定义的RequestMappingHandlerMapping。
创建WebMvcRegistrationsConfig类,重写getRequestMappingHandlerMapping( )的方法,将之前创建的ApiRequestMappingHandlerMapping注册到系统中。
public class WebMvcRegistrationsConfig implements WebMvcRegistrations {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new ApiRequestMappingHandlerMapping();
}
}
通过以上4步完成API版本控制的配置。代码看起来复杂,其实都是重写Spring Boot内部的处理流程。
步骤05 配置实现接口。
配置完成之后,接下来编写测试的控制器(Controller),实现相关接口的测试。在Controller目录下分别创建OrderV1Controller和OrderV2Controller。示例代码如下:
//V1 版本的接口定义
@ApiVersion(value = 1)
@RestController
@RequestMapping("/api/{version}/order")
public class OrderV1Controller {
@GetMapping("/delete/{orderId}")
public JSONResult deleteOrderById(@PathVariable String orderId) {
System.out.println("V1删除订单成功:" +orderId);
return JSONResult.ok("V1删除订单成功");
}
@GetMapping("/detail/{orderId}")
public JSONResult deleteOrderById(@PathVariable String orderId) {
System.out.println("V1获取订单详情成功:" +orderId);
return JSONResult.ok("V1获取订单详情成功");
}
}
//V2 版本的接口定义
@ApiVersion(value = 2)
@RestController
@RequestMapping("/api/{version}/order")
public class OrderV2Controller {
@GetMapping("/delete/{orderId}")
public JSONResult deleteOrderById(@PathVariable String orderId) {
System.out.println("V2获取订单详情成功:" +orderId);
return JSONResult.ok("V2获取订单详情成功");
}
@GetMapping("/list")
public JSONResult list() {
System.out.println("V2,获取list订单列表接口");
return JSONResult.ok(200, "V2,新增list订单列表接口");
}
}
在上面的示例中,我们在UserV1Controller中定义了/delete/{orderId}和/detail/{orderId}两个接口,在UserV2Controller中修改/detail/{orderId}接口,新增/list接口,然后使用@ApiVersion自定义注解设置两个Controller的版本号。
步骤06 验证测试。
配置完成之后启动项目,查看版本控制是否生效。在浏览器中分别访问api/v1/order/delete/20011和api/v2/order/ delete/20011订单删除接口,查看页面返回情况。如下图所示,调用V1和V2版本的order/ delete/20011订单删除接口,返回的都是“V1,删除订单成功”。这说明V2会默认继承V1的所有接口,新版本的原有接口功能保持不变。
接下来,在浏览器中分别访问api/v1/order/detail/20011和api/v2/order/delete/20011订单详情接口,查看页面返回情况。如下图所示,分别调用V1和V2版本的order/detail/20011订单详情接口,返回的是各自版本的接口信息,说明V2版本对order/detail订单详情接口的修改生效,同时也没有影响旧版本的订单详情接口。
最后,分别访问新增的order/list订单列表接口,查页面返回情况。如下图所示,请求V1的order/list订单列表返回404接口不存在,请求V2的order/list订单列表返回正确的结果,说明在高版本中新增的接口只在高版本中生效。
以上验证情况说明Web API的版本控制配置成功,实现了旧版本的稳定和新版本的更新。
1)当请求正确的版本地址时,会自动匹配版本的对应接口。
2)当请求的版本大于当前版本时,默认匹配最新的版本。
3)高版本会默认继承低版本的所有接口。实现版本升级只关注变化的部分,没有变化的部分会自动平滑升级,这就是所谓的版本继承。
4)高版本的接口的新增和修改不会影响低版本。
这些特性使得在升级接口时,原有接口不受影响,只关注变化的部分,没有变化的部分自动平滑升级。这样使得Web API更加简洁,这就是实现Web API版本控制的意义所在。