SpringBoot中关于是要 RestFull api版本控制你知道多少

科技   2024-09-27 08:04   河北  


前言

在实际开发中,由于业务需求的变更或协议本身的优化,HTTP接口的设计可能会经历显著的变化。为了确保这些变化不会对现有系统造成破坏,同时支持新功能的引入,我们需要在接口URL中明确指定版本号,以此来实现相同接口在不同版本下支持不同业务逻辑的目的。例如,原本用于添加用户的接口可能从/v1/user/add演变为/v2/user/add,以反映接口字段属性或逻辑的重大变更。为了有效管理这些不同版本的接口,在代码层面,我们应当采取一系列措施来确保每个版本的接口都能被清晰地区分、独立地维护,并且能够平滑地过渡。这包括但不限于使用模块化设计来隔离不同版本的业务逻辑,采用接口适配器模式来减少代码重复和耦合,以及利用路由工厂来动态地注册和管理路由。同时,我们还需要为每个版本的接口编写详尽的文档和测试,以确保其功能的正确性和稳定性,并随着时间的推移,逐步淘汰那些不再需要或维护的旧版本接口。

例如:

  • http://localhost:8080/v1/my/test

  • http://localhost:8080/v2/my/test 不兼容上一个版本

二、版本控制

对于历史版本的API支持,必须设定明确的时间限制和用户迁移策略。即,老版本的API在支持一段时间后应当被逐步淘汰,并鼓励新用户采用新版本API。否则,若API版本无限制地累积,达到10个甚至更多,将会给平台的维护带来极大的负担和复杂性。

API的变更在日常开发中虽属常态,但若变更频率过高,则需重新审视初始需求是否明确,以及每次变更API版本的真正目的何在。同时,还需关注不同版本API的使用者群体,确保变更不会对他们造成不必要的困扰。

值得注意的是,若API版本的更新仅仅是为了增加功能(如从不支持附件上传到支持附件上传),这种变更可能并不足以成为版本升级的理由。在此情况下,更合理的做法是单独发布一个专门用于附件上传的接口,以此降低不同功能之间的耦合度,使系统更加清晰和易于维护。

在开发接口的过程中,我们应始终铭记:功能的颗粒度应尽量细化(这需要在需求分析和模块设计阶段就予以考虑)。避免创建功能过于复杂、多合一的接口,这样的接口往往难以维护和扩展。

此外,为了保持版本的向下兼容性,我们应尽量通过增加字段和功能来实现新版本的迭代,而非删减现有功能。这样做可以确保旧版本客户端在升级前仍能正常使用API,减少因版本变更而带来的用户迁移成本和风险。

三、代码示例

在SpringMVC中RequestMappingHandlerMapping是比较重要的一个角色,它决定了每个URL分发至哪个Controller。
Spring Boot加载过程如下,所以我们可以通过自定义WebMvcRegistrationsAdapter来改写RequestMappingHandlerMapping。

通过在controller类和方法上添加注解@RequestMapping和自定义的@APIVersion(假设这是一个自定义注解用于处理版本信息),我们可以分别指定接口的URL和所需的版本号。DispatcherServlet作为Spring MVC的核心组件,负责解析进入的HTTP请求,并查找与之匹配的method handler(即控制器中的方法)。

DispatcherServlet的内部逻辑首先会遍历所有通过@RequestMapping注解标记的method handler,检查它们是否与当前请求的URL(包括path、method、header等条件,如果@RequestMapping中指定了这些条件)相匹配。在找到匹配的method handler候选集后,DispatcherServlet会进一步检查这些候选方法上是否使用了@APIVersion注解,并验证注解中指定的版本号是否与请求URL中指定的版本号相匹配。

最终,DispatcherServlet会选择那个既满足URL匹配条件又满足版本号匹配条件的method handler来处理当前的HTTP请求。这种方式确保了不同版本的API能够被正确地路由到相应的处理逻辑上,同时也为API的版本管理提供了一种灵活且易于扩展的机制。

  • 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.测试结果





Java技术前沿
专注分享Java技术,包括但不限于 SpringBoot,SpringCloud,Docker,消息中间件等。
 最新文章