分布式架构设计之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<ApiVerCondition> getCustomTypeCondition(Class<?>handlerType) {

       ApiVer apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVer.class);

       returncreateCondition(apiVersion);

    }

 

    // 映射匹配自定义注解(ApiVer标注在方法级别)

    protected RequestCondition<ApiVerCondition>  getCustomMethodCondition(Method method){

       ApiVer apiVersion = AnnotationUtils.findAnnotation(method, ApiVer.class);

       return createCondition(apiVersion);

    }

 

    // 获取当前控制器标注的版本,初始化Api版本条件

    // 参考 @ApiVerCondition注释说明.

    private RequestCondition<ApiVerCondition>  createCondition(ApiVer apiVersion) {

       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(新))

 

 

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐