我喜欢你,可是你却并不知道.

上一章简单介绍了SpringBoot整合Swagger(三十九),如果没有看过,请观看上一章

一. 为什么进行版本控制

一般来说,Web API是提供给其他系统或其他公司使用的,不能随意频繁地变更。

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

例如,系统中用户添加的接口/api/user由于业务需求的变化,接口的字段属性也发生了变化,而且可能与之前的功能不兼容。

为了保证原有的接口调用方不受影响,只能重新定义一个新的接口:/api/user2,这使得接口非常臃肿难看,而且极难维护

那么如何做到在不影响现有调用方的情况下,优雅地更新接口的功能呢?最简单高效的办法就是对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等,然后根据不同的版本选择执行不同的方法。

在实际项目开发中,一般选择第二种方式,因为这样既能保证水平扩展,又不影响以前的老版本。

二. Web Api 版本控制

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

二.一 创建自定义的 @ApiVersion 注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 创建自定义的注解
 *
 * @author yuejianli
 * @date 2023-04-04
 */

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
    /**
     * @return版本号
     */
    int value() default 1;
}

二.二 自定义 URL 匹配逻辑

定义URL匹配逻辑,创建ApiVersionCondition类并继承RequestCondition接口,其作用是进行版本号筛选,

将提取请求URL中的版本号与注解上定义的版本号进行对比,以此来判断某个请求应落在哪个控制器上

/**
 * 自定义URL匹配逻辑
 *
 * @author yuejianli
 * @date 2023-04-04
 */

import org.springframework.web.servlet.mvc.condition.RequestCondition;
import javax.servlet.http.HttpServletRequest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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是否符合版本要求

二.三 自定义匹配的处理程序

先创建ApiRequestMappingHandlerMapping类,重写部分RequestMappingHandlerMapping的方法,实现自定义的匹配处理程序

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 top.yueshushu.learn.annotation.ApiVersion;

import java.lang.reflect.Method;

/**
 * 自定义匹配的处理程序
 *
 * @author yuejianli
 * @date 2023-04-04
 */

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);
    }
}

二.四 注册自定义的 Mapping 到系统中

@Configuration
public class WebMvcRegistrationsConfig implements WebMvcRegistrations {
    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new ApiRequestMappingHandlerMapping();
    }
}

二.五 配置实现接口

二.五.一 V1 版本

有 删除 和查询的功能

@RestController
@ApiVersion(value = 1)
@RequestMapping("/api/{version}/user")
@Slf4j
public class User1Controller {

    @GetMapping("/delete/{id}")
    public String deleteById(@PathVariable("id") Integer id) {
      // 执行删除的业务处理
      return "V1 版本删除用户";
    }

    @GetMapping("/get/{id}")
    public String getById(@PathVariable("id") Integer id) {
        // 执行 查询的业务处理
        return "V1 版本 查询用户";
    }
}

二.五.二 V2 版本

继承了 V1 的接口, 修改了 get 的方法, 并扩展了 change 接口

@RestController
@ApiVersion(value = 2)
@RequestMapping("/api/{version}/user")
@Slf4j
public class User2Controller {

    @GetMapping("/get/{id}")
    public String getById(@PathVariable("id") Integer id) {
        // 执行 查询的业务处理
        return "V2 版本 查询用户";
    }

    @GetMapping("/change/{id}")
    public String changeById(@PathVariable("id") Integer id) {
        // 执行 改变的 业务处理
        return "V2 版本改变用户";
    }
}

二.六 测试验证

访问 v1 的删除

SpringBoot 实现 Web 版本控制 (四十)_版本控制

访问 v2 的删除, 内部会调用 V1的 , 继承了 V1的接口

SpringBoot 实现 Web 版本控制 (四十)_java_02

访问 V1 的查询

SpringBoot 实现 Web 版本控制 (四十)_API_03

访问 V2 的查询

SpringBoot 实现 Web 版本控制 (四十)_Web版本控制_04

会变成新的内部处理

访问 V2 新增加的 change

SpringBoot 实现 Web 版本控制 (四十)_java_05

V1 是没有 change 的

SpringBoot 实现 Web 版本控制 (四十)_版本控制_06

以上验证情况说明Web API的版本控制配置成功,实现了旧版本的稳定和新版本的更新。

1)当请求正确的版本地址时,会自动匹配版本的对应接口。

2)当请求的版本大于当前版本时,默认匹配最新的版本。

3)高版本会默认继承低版本的所有接口。实现版本升级只关注变化的部分,没有变化的部分会自动平滑升级,这就是所谓的版本继承。

4)高版本的接口的新增和修改不会影响低版本。

这些特性使得在升级接口时,原有接口不受影响,只关注变化的部分,没有变化的部分自动平滑升级。

这样使得Web API更加简洁,这就是实现Web API版本控制的意义所在。



本章节的代码放置在 github 上:

https://github.com/yuejianli/springboot/tree/develop/SpringBoot_Version


谢谢您的观看,如果喜欢,请关注我,再次感谢 !!!