1.楔子

设计一套基于http协议的业务接口,但是随着时间变迁,业务的变化,或者我们协议本身的优化,都有可能要改变之前存在的接口。这时候给所有接口进行版本管理就显得很重要了,比如某个添加用户的接口,由于业务发展很大,接口的字段属性变化很大,只能重新定义一个新的接口,由 /v1/user/add 变成了 /v2/user/add,这样我们就要维护两套接口的逻辑,映射到代码里,就是要维护两个不同的业务方法。如何方便我们在代码层级管理各不同的版本接口?

在实际使用api接口可能由于某种原因接口功能做了不兼容的改变,这时就需要通过在接口URL中指定版本号以达到使相同的接口支持不同的业务的目的。例如:

2.版本控制策略

在这里插入图片描述
设计建议:

  • 对历史版本的API支持一定要有时间和用户限制,即老版API支持到一定时间就删除,新用户必须使用新版API,否则一个API有10个版本会让平台的维护非常痛苦。
  • API日常接口变更不可避免,但若变更太频繁,就要思考最初的需求是不是有问题,变更API版本的目的是什么,每个版本使用用户都不同等等方面的问题;
  • 如果不同版本的API仅是功能更强大了,比如原来不支持附件上传,新版本支持附件上传,这种场景的API版本变更就没意义,更合理的是单独发布一个附件上传的接口,降低功能间的耦合。
  • 在开发接口时始终要记住一点:尽量将功能的颗粒度降到最低(需求分析、模块设计阶段),不要做多功能融合的接口。
  • 尽量做到字段和功能的增加,这样就能保证版本的向下兼容。少做功能的删减。

详见:

3. 实现

在SpringMVC中RequestMappingHandlerMapping是比较重要的一个角色,它决定了每个URL分发至哪个Controller。
Spring Boot加载过程如下,所以我们可以通过自定义WebMvcRegistrationsAdapter来改写RequestMappingHandlerMapping。
在这里插入图片描述
通过在controller类和方法上添加注解@RequestMapping和@APIVersion,分别指定接口URL和版本号要求。DispatcherServlet的内部逻辑解析request,查找匹配的method handler。首先会查找@RequestMapping注解过的method handler是否与request匹配(检查request的path、method、header等),然后检查method handler的@ApiVersion注解的版本号是否与request url中的版本号匹配,最终选择符合条件的method handler。

  • 1.自定义版本控制的注解
/**
 * API版本控制注解
 * Created on 2019/4/18 11:17.
 * @author caogu
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
    //标识版本号
    int value();
}
  • 2.自定义url匹配逻辑
    当方法级别和类级别都有ApiVersion注解时,二者将进行合并(ApiVersionRequestCondition.combine)。最终将提取请求URL中版本号,与注解上定义的版本号进行比对,判断url是否符合版本要求。
/**
 * 自定义url匹配逻辑
 * Created on 2019/4/18 14:07.
 *
 * @author caogu
 */
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
    // 路径中版本的前缀, 这里用 /v[1-9]/的形式
    private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+)/");

    //api的版本
    private int apiVersion;

    public ApiVersionCondition(int apiVersion) {
        this.apiVersion = apiVersion;
    }

    //将不同的筛选条件合并
    @Override
    public ApiVersionCondition combine(ApiVersionCondition apiVersionCondition) {
        // 采用最后定义优先原则,则方法上的定义覆盖类上面的定义
        return new ApiVersionCondition(apiVersionCondition.getApiVersion());
    }


    //根据request查找匹配到的筛选条件
    @Override
    public ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {
        //return null;
        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 0;
        // 优先匹配最新的版本号
        return apiVersionCondition.getApiVersion() - this.apiVersion;
    }

    public int getApiVersion() {
        return apiVersion;
    }
}
  • 3.自定义匹配的处理器
@Configuration
public class ApiVersionConfig extends WebMvcConfigurationSupport {

    //重写请求过处理的方法
    @Override
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping();
        handlerMapping.setOrder(0);
        return handlerMapping;
    }

    /**
     * 自定义匹配的处理器
     * Created on 2019/4/18 14:10.
     *
     * @author caogu
     */
    private static class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
        @Override
        protected RequestCondition<ApiVersionCondition> getCustomTypeCondition(Class<?> handlerType) {
            ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
            return createCondition(apiVersion);
        }

        @Override
        protected RequestCondition<ApiVersionCondition> getCustomMethodCondition(Method method) {
            ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
            return createCondition(apiVersion);
        }

        private RequestCondition<ApiVersionCondition> createCondition(ApiVersion apiVersion) {
            return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value());
        }
    }
}
  • 4.编写测试的控制器
@RestController
@ApiVersion(1)
@RequestMapping("{version}/my/")
public class ApiVersion1Controller {

    @RequestMapping("test")
    public String test() {
        return "OK! v1";
    }

}

@RestController
@RequestMapping("{version}/my/")
@ApiVersion(1)
public class ApiVersion2Controller {

    @RequestMapping("test")
    @ApiVersion(2)// 方法会覆盖类的版本
    public String test() {
        return "OK! v2";
    }

}
  • 5.测试结果
    在这里插入图片描述
    在这里插入图片描述在这里插入图片描述
    在这里插入图片描述
    首先会找匹配版本的接口,如果没有则使用比自己请求版本号小 的最大服务版本号。

4. 其他实现思路

思路:用户请求url —> 拦截器拦截 —> 转发到真正处理类和方法 —> 返回结果

5. 参考文献

Logo

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

更多推荐