实现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;
}

在上面的示例中,创建了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;
    }
}

当方法级别和类级别都有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);
    }
}

步骤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();
    }
}

通过以上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.*;

/**
 * <p>
 * 用户表 前端控制器
 * </p>
 *
 * @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);
    }
}

在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;

/**
 * <p>
 * 用户表 前端控制器
 * </p>
 *
 * @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);
    }
}

现api版本如下:

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

测试1

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

访问版本1:

/malldemo/api/v1/user/admin

响应内容:

{
  "code": 200,
  "msg": "OK",
  "data": [
    {
      "userCode": "admin",
      "userName": "管理员",
      "memo": "测试数据!"
    }
  ]
}

访问版本2、3、4:

/malldemo/api/v2/user/admin
/malldemo/api/v3/user/admin
/malldemo/api/v4/user/admin

响应内容:

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

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

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


测试2

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

访问版本1:

/malldemo/api/v1/user/data

# 参数均为
current=1
size=2

响应内容:

{
  "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
  }
}

访问版本2、3、4:

/malldemo/api/v2/user/data
/malldemo/api/v3/user/data
/malldemo/api/v4/user/data

# 参数均为
current=1
size=3

响应内容:

{
  "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
  }
}

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


小结

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

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

说明

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

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.2</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<!--        启用web支持-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--        启用lombok(lombok 与 日志@Slf4j)-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<!--        启用单元测试-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<!-- mybatis-plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.2</version>
</dependency>


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

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值