• 实现Web API版本控制-springboot


    为什么进行版本控制

    由于需求和业务不断变化,Web API也会随之不断修改。如果直接对原来的接口修改,势必会影响其他系统的正常运行。

    那么如何做到在不影响现有调用方的情况下,优雅地更新接口的功能呢?最简单高效的办法就是对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等,然后根据不同的版本选择执行不同的方法。在实际项目开发中,一般选择第二种方式,因为这样既能保证水平扩展,又不影响以前的老版本。

    实现版本控制

    Spring Boot对RESTful的支持非常全面,因而实现RESTful API非常简单,同样对于API版本控制也有相应的实现方案:

    • 1)创建自定义的@APIVersion注解。
    • 2)自定义URL匹配规则ApiVersionCondition。
    • 3)使用RequestMappingHandlerMapping创建自定义的映射处理程序,根据Request参数匹配符合条件的处理程序。

    步骤01

    创建自定义注解

    package com.qsdbl.malldemo.configuration.apiversion;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @author: 轻率的保罗
     * @since: 2022-11
     * @description Web API 版本控制。步骤01、创建自定义注解
     */
    
    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ApiVersion {
        /**
         * @return版本号
         */
        int value() default 1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在上面的示例中,创建了ApiVersion自定义注解用于API版本控制,并返回了对应的版本号。


    步骤02

    自定义URL匹配逻辑

    package com.qsdbl.malldemo.configuration.apiversion;
    
    import org.springframework.web.servlet.mvc.condition.RequestCondition;
    import javax.servlet.http.HttpServletRequest;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    /**
     * @author: 轻率的保罗
     * @since: 2022-11
     * @Description: 步骤02、自定义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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    当方法级别和类级别都有ApiVersion注解时,通过ApiVersionRequestCondition.combine方法将二者进行合并。最终将提取请求URL中的版本号,与注解上定义的版本号进行对比,判断URL是否符合版本要求。


    步骤03

    自定义匹配的处理程序

    package com.qsdbl.malldemo.configuration.apiversion;
    
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.servlet.mvc.condition.RequestCondition;
    import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
    
    import java.lang.reflect.Method;
    
    /**
     * @author: 轻率的保罗
     * @since: 2022-11
     * @Description: 步骤03、自定义匹配的处理程序
     */
    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<?> handlerType) {
            return createCondition(handlerType);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    步骤04

    配置注册自定义的RequestMappingHandlerMapping

    package com.qsdbl.malldemo.configuration.apiversion;
    
    import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
    
    /**
     * @author: 轻率的保罗
     * @since: 2022-11
     * @Description: 步骤04、配置注册自定义的RequestMappingHandlerMapping
     */
    @Configuration
    public class WebMvcRegistrationsConfig implements WebMvcRegistrations {
        @Override
        public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
            return new ApiRequestMappingHandlerMapping();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    通过以上4步完成API版本控制的配置。代码看起来复杂,其实都是重写Spring Boot内部的处理流程。


    步骤05

    使用自定义注解ApiVersion改造Controller

    package com.qsdbl.malldemo.web;
    
    import com.qsdbl.malldemo.configuration.apiversion.ApiVersion;
    import com.qsdbl.malldemo.entity.SysUserEntity;
    import com.qsdbl.malldemo.common.dto.DataVo;
    import com.qsdbl.malldemo.mapper.SysUserMapper;
    import com.qsdbl.malldemo.service.impl.SysUserServiceImp;
    import com.qsdbl.malldemo.common.utils.JSONResult;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import io.swagger.annotations.ApiParam;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;
    
    /**
     * 

    * 用户表 前端控制器 *

    * * @author 轻率的保罗 * @since 2022-11 */
    @Slf4j @RestController @ApiVersion(value = 1) @RequestMapping("api/{version}/user") @Api(tags = "用户数据 前端控制器") public class SysUserController { /** * 用户表 Mapper对象 */ @Autowired private SysUserMapper userMapper; /** * 用户表 Mapper对象 */ @Autowired private SysUserServiceImp userService; @ApiOperation("查询所有用户数据") @GetMapping("/all") public DataVo queryAll(){ return JSONResult.ok(userMapper.selectList(null)); } @ApiOperation(value = "分页查询用户数据",notes = "可添加查询过滤条件,为like模糊查询") @GetMapping("/data") public DataVo queryPage(@ApiParam(value = "当前页",required = true) int current, @ApiParam(value = "页面大小",required = true) int size, @ApiParam("查询条件,字段模糊查询") SysUserEntity user){ return userService.queryPage(current, size, user); } @ApiOperation("根据账号查找用户") @GetMapping("/{userCode}") public DataVo queryBycode(@PathVariable @ApiParam("账号") String userCode){ return userService.queryBycode(userCode); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    在Controller上,添加/修改如下注解(可使用在方法级别类级别):

    @ApiVersion(value = 1)
    @RequestMapping(“api/{version}/user”)

    表名该Controller中的api版本为“1”,路径中使用的参数{version}将会被转换为v1(见步骤2中的配置)。


    测试

    结论:升级接口时,原有接口不受影响,只关注变化的部分,没有变化的部分自动平滑升级。

    添加一个SysUserControllerV2测试。

    将api“查询所有用户数据”、“查询所有用户数据”注释掉,修改api“根据账号查找用户”的返回信息。

    package com.qsdbl.malldemo.web;
    
    import com.qsdbl.malldemo.common.dto.DataVo;
    import com.qsdbl.malldemo.common.utils.JSONResult;
    import com.qsdbl.malldemo.configuration.apiversion.ApiVersion;
    import com.qsdbl.malldemo.entity.SysUserEntity;
    import com.qsdbl.malldemo.mapper.SysUserMapper;
    import com.qsdbl.malldemo.service.impl.SysUserServiceImp;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import io.swagger.annotations.ApiParam;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * 

    * 用户表 前端控制器 *

    * * @author 轻率的保罗 * @since 2022-11 */
    @Slf4j @RestController @ApiVersion(value = 2) @RequestMapping("api/{version}/user") @Api(tags = "用户数据 前端控制器") public class SysUserControllerV2 { /** * 用户表 Mapper对象 */ @Autowired private SysUserMapper userMapper; /** * 用户表 Mapper对象 */ @Autowired private SysUserServiceImp userService; // @ApiOperation("查询所有用户数据") // @GetMapping("/all") // public DataVo queryAll(){ // return JSONResult.ok(userMapper.selectList(null)); // } // @ApiOperation(value = "分页查询用户数据",notes = "可添加查询过滤条件,为like模糊查询") // @GetMapping("/data") // public DataVo queryPage(@ApiParam(value = "当前页",required = true) int current, // @ApiParam(value = "页面大小",required = true) int size, // @ApiParam("查询条件,字段模糊查询") SysUserEntity user){ // return userService.queryPage(current, size, user); // } @ApiOperation("根据账号查找用户") @GetMapping("/{userCode}") public DataVo queryBycode(@PathVariable @ApiParam("账号") String userCode){ return JSONResult.build(200,"第二版本的api,目前开发中,请先使用第一个版本的api!!!",null); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    现api版本如下:

    • 版本1:
      • 查询所有用户数据
      • 查询所有用户数据
      • 根据账号查找用户
    • 版本2
      • 根据账号查找用户

    测试1

    访问两个版本的api“根据账号查找用户”

    访问版本1:

    /malldemo/api/v1/user/admin
    
    • 1

    响应内容:

    {
      "code": 200,
      "msg": "OK",
      "data": [
        {
          "userCode": "admin",
          "userName": "管理员",
          "memo": "测试数据!"
        }
      ]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    访问版本2、3、4:

    /malldemo/api/v2/user/admin
    /malldemo/api/v3/user/admin
    /malldemo/api/v4/user/admin
    
    • 1
    • 2
    • 3

    响应内容:

    {
      "code": 200,
      "msg": "第二版本的api,目前开发中,请先使用第一个版本的api!!!",
      "data": null
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    当请求正确的版本地址时,会自动匹配版本的对应接口 - 版本1、版本2均能正常访问。

    当请求的版本大于当前版本时,默认匹配最新的版本 - 虽然不存在v3、v4版本,但依然能访问,匹配的是最高的版本v2而不是v1。


    测试2

    访问两个版本的api“根据账号查找用户”

    访问版本1:

    /malldemo/api/v1/user/data
    
    # 参数均为
    current=1
    size=2
    
    • 1
    • 2
    • 3
    • 4
    • 5

    响应内容:

    {
      "code": 200,
      "msg": "OK",
      "data": {
        "records": [
          {
            "userCode": "gaoshoujun",
            "userName": "高守君",
            "memo": "测试数据!"
          },
          {
            "userCode": "gongjin",
            "userName": "龚金",
            "memo": "测试数据!"
          }
        ],
        "total": 102,
        "size": 2,
        "current": 1,
        "orders": [],
        "optimizeCountSql": true,
        "searchCount": true,
        "countId": null,
        "maxLimit": null,
        "pages": 51
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    访问版本2、3、4:

    /malldemo/api/v2/user/data
    /malldemo/api/v3/user/data
    /malldemo/api/v4/user/data
    
    # 参数均为
    current=1
    size=3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    响应内容:

    {
      "code": 200,
      "msg": "OK",
      "data": {
        "records": [
          {
            "userCode": "gaoshoujun",
            "userName": "高守君",
            "memo": "测试数据!"
          },
          {
            "userCode": "gongjin",
            "userName": "龚金",
            "memo": "测试数据!"
          },
          {
            "userCode": "manannan",
            "userName": "马楠楠",
            "memo": "测试数据!"
          }
        ],
        "total": 102,
        "size": 3,
        "current": 1,
        "orders": [],
        "optimizeCountSql": true,
        "searchCount": true,
        "countId": null,
        "maxLimit": null,
        "pages": 34
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    当请求的版本大于当前版本时,默认匹配最新的版本 - 虽然不存在v2、v3、v4版本,但依然能访问,匹配的是最高的版本v1。


    小结

    实现了旧版本的稳定和新版本的更新。

    • 1)当请求正确的版本地址时,会自动匹配版本的对应接口。
    • 2)当请求的版本大于当前版本时,默认匹配最新的版本。
    • 3)高版本会默认继承低版本的所有接口。实现版本升级只关注变化的部分,没有变化的部分会自动平滑升级,这就是所谓的版本继承。
    • 4)高版本的接口的新增和修改不会影响低版本。

    说明

    本博客中的案例,使用的maven依赖如下:

    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.5.2version>
        <relativePath/> 
    parent>
    
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    
    
    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
        <optional>trueoptional>
    dependency>
    
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-testartifactId>
        <scope>testscope>
    dependency>
    
    
    <dependency>
        <groupId>com.baomidougroupId>
        <artifactId>mybatis-plus-boot-starterartifactId>
        <version>3.5.2version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33


    笔记摘自:《Spring Boot从入门到实战》-章为忠

  • 相关阅读:
    lv19 多态 4
    李峋同款会动的爱心Python代码版
    【C++杂货铺】一文带你走进RBTree
    魔兽WOW外网搭建的新手教程
    【学习笔记39】获取DOM标签对象
    单商户商城系统功能拆解10—商城风格
    RK3568驱动指南|第五期-中断-第45章 自定义工作队列实验
    【Java成王之路】EE初阶第十四篇:(网络原理) 4
    【JavaScript】JavaScript 运算符 ① ( 运算符分类 | 算术运算符 | 浮点数 的 算术运算 精度问题 )
    【ChatGLM2-6B】在只有CPU的Linux服务器上进行部署
  • 原文地址:https://blog.csdn.net/weixin_44773109/article/details/128068682