分布式架构设计之RestAPI版本管理
随着互联网发展脚步的加快,产品项目的迭代也随之加快,所以就需要我们对产品的稳定提供一定的保障。而直接与用户接触的前端应用一般都是通过接口API与后台交互,一旦相关的API需求改版后,原来的API就不能使用,需要重新发布更新,如果前端产品是移动APP应用,比如:android/ios,那么就必须重新提交应用审核,等待若干天的审核发布是很不好的,严重影响用户的使用,所以建立API版本,使新改版的接口API不影响老版本的API使用,就显得很有必要了,那么接下来就介绍下。
l 版本形式
l 版本实现
l 版本方案
一、版本形式
API版本形式主要指的是API版本接口地址的形式,目前比较常见的形式如下:
1、HTPP地址参数
地址形式:http://server:port/api/xxx?v=yyy
xxx代表具体的接口名字,?号后面跟着参数v,该参数为具体的API版本号,需要客户端传递过来,此中版本形式比较传统。
2、HTTP请求头部
地址形式:http://server:port/api/xxx
请求头部:
即在API请求头部添加Accept属性,用于指定响应接收的媒体类型(Media Type),常见Accept形式如下:
application/vnd.api-v1+json
3、HTTP Rest风格
地址形式:http://server:port/api/{v}/xxx
上面的{v}为动态替换的参数,其作为接口地址API的一部分,是REST风格的地址形式,所以在灵活度及前后端通信协议的耦合度上都有优势。而在下面的版本实现时,就是采用这种方式实现验证的。
二、版本实现
在这里,我会使用Java语言的SSM框架来演示API版本的实现。并且全文中仅罗列实现的核心部分,其它内容需要读者具备一定的基础。
1、ApiVer.java
/**
* 自定义接口版本标注注解,该注解可标注在
* 类及方法函数上,并且在运行时环境起作用,
* 并且可以被潜入到javadoc中,同时指定该
* 标签为web映射注解.
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVer {
intvalue();
}
2、ApiRequestMapping.java
/**
* 首先,需要在Servlet配置中注册该类
* 其次,动态编译时匹配ApiVersion标签,
* 获取控制器中对应类或方法的版本值.
*/
public class ApiRequestMapping extends RequestMappingHandlerMapping {
// 映射匹配自定义注解(ApiVer标注在类级别)
protected RequestCondition
ApiVer apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVer.class);
returncreateCondition(apiVersion);
}
// 映射匹配自定义注解(ApiVer标注在方法级别)
protected RequestCondition
ApiVer apiVersion = AnnotationUtils.findAnnotation(method, ApiVer.class);
return createCondition(apiVersion);
}
// 获取当前控制器标注的版本,初始化Api版本条件
// 参考 @ApiVerCondition注释说明.
private RequestCondition
return apiVersion ==null ?null :newApiVerCondition(apiVersion.value());
}
}
3、ApiVerCondition.java
/**
* 首先,匹配过滤出当前访问接口中是否存在v(1-9),
* 如果存在,继续判断格式是否对,否则报错返回。
* 其次,比较请求地址中版本与控制器中版本,如果
* 请求的版本值大于控制器中版本值,则取控制器版本
* 值继续接口后续访问操作。
*/
public class ApiVerCondition implements RequestCondition<ApiVerCondition>{
private final static PatternVERSION_PREFIX_PATTERN =Pattern.compile("v(\\d+)/");
private int apiVersion;
public ApiVerCondition(int apiVersion){
this.apiVersion = apiVersion;
}
public ApiVerCondition combine(ApiVerConditionother) {
return new ApiVerCondition(other.getApiVersion());
}
public ApiVerCondition getMatchingCondition(HttpServletRequest request) {
Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI());
if(m.find()){
Integer version = Integer.valueOf(m.group(1));
if(version >=this.apiVersion)
return this;
}
return null;
}
public int compareTo(ApiVerCondition other, HttpServletRequest request) {
return other.getApiVersion() -this.apiVersion;
}
public int getApiVersion() {
return apiVersion;
}
}
这里值得说明下,当前端访问的接口地址版本值大于控制器中配置的接口值时,则就会自动访问当前接口版本最大的接口方法,只做了向上最大兼容。
4、ServletConfig.java
@Configuration
@EnableWebMvc
@ComponentScan("com.cwteam.demo.action")
public class ServletConfig extends WebMvcConfigurerAdapter {
@Bean
public RequestMapping HandlerMapping requestMappingHandlerMapping() {
RequestMappingHandlerMapping handlerMapping =new ApiRequestMapping();
return handlerMapping;
}
}
这里主要是在SpringMVC中注册请求映射拦截转发。
5、ApiVerAction.java
/**
* API控制器入口:
* 首先,根控制器为动态版本{v},供前端传递;
* 其次,在接口方法上标注@ApiVer自定义注解;
* 最后,访问同一个地址时,如:http://server:port/v1/hello,
* 结果返回hello world v1
*/
@RestController
@RequestMapping("/{v}")
public class ApiVerAction {
@ApiVer(1)
@RequestMapping(value="/hello",method=RequestMethod.GET)
public String helloV1() throws Exception {
return"hello world v1";
}
@ApiVer(2)
@RequestMapping(value="/hello",method=RequestMethod.GET)
public String helloV2() throws Exception {
return"hello world v2";
}
@ApiVer(3)
@RequestMapping(value="/hello",method=RequestMethod.GET)
public String helloV3() throws Exception {
return"hello world v3";
}
}
测试结果:
访问地址:
http://localhost:8080/restapi-version/v1/hello
访问地址:
http://localhost:8080/restapi-version/v2/hello
访问地址:
http://localhost:8080/restapi-version/v3/hello
访问地址:
http://localhost:8080/restapi-version/v4/hello
三、版本方案
我们知道,实际项目中API会存在很多,如果为每个API添加3~5个版本控制,相对维护成本还好(但也很人肉),如果需要支撑所有或几十个API版本兼容昵?如果完全按照上面的实现方式,答案是维护成本非常之高。另外,如果老用户已经很多版本没更新,我们也应该支持其API正常使用,这就需要对老版本进行兼容支持,所以需要在下面讨论下API的实际需要。
1、支持向上兼容
如上图,我们可以对老版本API兼容支持,也就是最新的API版本逻辑,能够同时满足新老用户需要,也就是我们常见到的单一API版本接口。此种版本方式优缺点都很明显,优势就是提供客户端统一一个接口,劣势就是随着版本功能的升级,该接口API内部功能会臃肿,不便于后续的维护开展。
2、不做向上兼容
如上图,与上面的API方式对比,这里对每一次的API功能改版升级都提供一个版本,也就是功能API会存在很多子版本。它的好处就是每个API的功能内部简洁突出当前版本功能,不兼容老版本的功能,如果是老用户的API,就直接客户端传递版本号,访问对应的版本API即可。劣势就是,日后会有成千上万的版本接口,后续维护升级的成本相当高。
3、用户版本升级
就长远角度考虑,我建议上面第2种实现方式,但又不能让API的版本无限递增下去,所以我们需要结合实际情况,只保留一定数量的API支持,比如:仅支持10个版本,如果低于这个版本,则强制老用户升级API,保证正常使用。
实际上,软件做得再完美,如果偏离了实际,那么一文不值,所以这里的API需要判断处理:如果老用户版本低于当前最新版本10个版本时,我们提示其强制更新,否则不能正常使用。当然,总有那么一些需求人提出,要支持所有的老版本API,虽然不建议这样实现,但也是可以做到的,比如:将API的版本功能实现以字节码形式,存放在数据库或文件或中间组件中,以方便对所有API版本进行管理,在加载编译之前,将所有的API版本加载到接口API实现逻辑中,带用户提交对应版本号时,自动定位到指定API,这样做可以支持更多历史版本,实际上一些API平台就是采用这种方式(是支持了更多版本,但也是有数量限制的,如果是合理设计的化)。
项目代码:
http://download.csdn.net/download/why_2012_gogo/10122734
好了,由于作者水平有限,如有不正确或是误导的地方,请不吝指出讨论(技术交流群:497552060(新))