• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

SpringBoot接口 - 提供多个版本接口

武飞扬头像
Ch3nnn
帮助1

在以SpringBoot开发Restful接口时,由于模块,系统等业务的变化,需要对同一接口提供不同版本的参数实现(老的接口还有模块或者系统在用,不能直接改,所以需要不同版本)。如何更加优雅的实现多版本接口呢?

为什么接口会出现多个版本?

为什么接口会出现多个版本?

一般来说,Restful API接口是提供给其它模块,系统或是其他公司使用,不能随意频繁的变更。然而,需求和业务不断变化,接口和参数也会发生相应的变化。如果直接对原来的接口进行修改,势必会影响线其他系统的正常运行。这就必须对api 接口进行有效的版本控制。

有哪些控制接口多版本的方式?

  • 相同URL,用不同的版本参数区分

    • api.pdai.tech/user?version=v1 表示 v1版本的接口, 保持原有接口不动
    • api.pdai.tech/user?version=v2 表示 v2版本的接口,更新新的接口
  • 区分不同的接口域名,不同的版本有不同的子域名, 路由到不同的实例:

    • v1.api.pdai.tech/user 表示 v1版本的接口, 保持原有接口不动, 路由到instance1
    • v2.api.pdai.tech/user 表示 v2版本的接口,更新新的接口, 路由到instance2
  • 网关路由不同子目录到不同的实例(不同package也可以)

    • api.pdai.tech/v1/user 表示 v1版本的接口, 保持原有接口不动, 路由到instance1
    • api.pdai.tech/v2/user 表示 v2版本的接口,更新新的接口, 路由到instance2
  • 同一实例,用注解隔离不同版本控制

    • api.pdai.tech/v1/user 表示 v1版本的接口, 保持原有接口不动,匹配@ApiVersion("1")的handlerMapping
    • api.pdai.tech/v2/user 表示 v2版本的接口,更新新的接口,匹配@ApiVersion("2")的handlerMapping

这里主要展示第四种单一实例中如何优雅的控制接口的版本。

实现案例

这个例子基于SpringBoot封装了@ApiVersion注解方式控制接口版本。

自定义@ApiVersion注解

  1.  
    package tech.pdai.springboot.api.version.config.version;
  2.  
     
  3.  
    import org.springframework.web.bind.annotation.Mapping;
  4.  
     
  5.  
    import java.lang.annotation.*;
  6.  
     
  7.  
    @Target({ElementType.METHOD, ElementType.TYPE})
  8.  
    @Retention(RetentionPolicy.RUNTIME)
  9.  
    @Documented
  10.  
    @Mapping
  11.  
    public @interface ApiVersion {
  12.  
    String value();
  13.  
    }

定义版本匹配RequestCondition

版本匹配支持三层版本

  • v1.1.1 (大版本.小版本.补丁版本)
  • v1.1 (等同于v1.1.0)
  • v1 (等同于v1.0.0)
  1.  
    package tech.pdai.springboot.api.version.config.version;
  2.  
     
  3.  
    import lombok.Getter;
  4.  
    import lombok.extern.slf4j.Slf4j;
  5.  
    import org.springframework.web.servlet.mvc.condition.RequestCondition;
  6.  
     
  7.  
    import javax.servlet.http.HttpServletRequest;
  8.  
    import java.util.Arrays;
  9.  
    import java.util.Collections;
  10.  
    import java.util.List;
  11.  
    import java.util.regex.Matcher;
  12.  
    import java.util.regex.Pattern;
  13.  
     
  14.  
    @Slf4j
  15.  
    public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
  16.  
     
  17.  
    /**
  18.  
    * support v1.1.1, v1.1, v1; three levels .
  19.  
    */
  20.  
    private static final Pattern VERSION_PREFIX_PATTERN_1 = Pattern.compile("/v\\d\\.\\d\\.\\d/");
  21.  
    private static final Pattern VERSION_PREFIX_PATTERN_2 = Pattern.compile("/v\\d\\.\\d/");
  22.  
    private static final Pattern VERSION_PREFIX_PATTERN_3 = Pattern.compile("/v\\d/");
  23.  
    private static final List<Pattern> VERSION_LIST = Collections.unmodifiableList(
  24.  
    Arrays.asList(VERSION_PREFIX_PATTERN_1, VERSION_PREFIX_PATTERN_2, VERSION_PREFIX_PATTERN_3)
  25.  
    );
  26.  
     
  27.  
    @Getter
  28.  
    private final String apiVersion;
  29.  
     
  30.  
    public ApiVersionCondition(String apiVersion) {
  31.  
    this.apiVersion = apiVersion;
  32.  
    }
  33.  
     
  34.  
    /**
  35.  
    * method priority is higher then class.
  36.  
    *
  37.  
    * @param other other
  38.  
    * @return ApiVersionCondition
  39.  
    */
  40.  
    @Override
  41.  
    public ApiVersionCondition combine(ApiVersionCondition other) {
  42.  
    return new ApiVersionCondition(other.apiVersion);
  43.  
    }
  44.  
     
  45.  
    @Override
  46.  
    public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
  47.  
    for (int vIndex = 0; vIndex < VERSION_LIST.size(); vIndex ) {
  48.  
    Matcher m = VERSION_LIST.get(vIndex).matcher(request.getRequestURI());
  49.  
    if (m.find()) {
  50.  
    String version = m.group(0).replace("/v", "").replace("/", "");
  51.  
    if (vIndex == 1) {
  52.  
    version = version ".0";
  53.  
    } else if (vIndex == 2) {
  54.  
    version = version ".0.0";
  55.  
    }
  56.  
    if (compareVersion(version, this.apiVersion) >= 0) {
  57.  
    log.info("version={}, apiVersion={}", version, this.apiVersion);
  58.  
    return this;
  59.  
    }
  60.  
    }
  61.  
    }
  62.  
    return null;
  63.  
    }
  64.  
     
  65.  
    @Override
  66.  
    public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
  67.  
    return compareVersion(other.getApiVersion(), this.apiVersion);
  68.  
    }
  69.  
     
  70.  
    private int compareVersion(String version1, String version2) {
  71.  
    if (version1 == null || version2 == null) {
  72.  
    throw new RuntimeException("compareVersion error:illegal params.");
  73.  
    }
  74.  
    String[] versionArray1 = version1.split("\\.");
  75.  
    String[] versionArray2 = version2.split("\\.");
  76.  
    int idx = 0;
  77.  
    int minLength = Math.min(versionArray1.length, versionArray2.length);
  78.  
    int diff = 0;
  79.  
    while (idx < minLength
  80.  
    && (diff = versionArray1[idx].length() - versionArray2[idx].length()) == 0
  81.  
    && (diff = versionArray1[idx].compareTo(versionArray2[idx])) == 0) {
  82.  
    idx;
  83.  
    }
  84.  
    diff = (diff != 0) ? diff : versionArray1.length - versionArray2.length;
  85.  
    return diff;
  86.  
    }
  87.  
    }
学新通

定义HandlerMapping

  1.  
    package tech.pdai.springboot.api.version.config.version;
  2.  
     
  3.  
    import org.springframework.core.annotation.AnnotationUtils;
  4.  
    import org.springframework.lang.NonNull;
  5.  
    import org.springframework.web.servlet.mvc.condition.RequestCondition;
  6.  
    import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
  7.  
     
  8.  
    import java.lang.reflect.Method;
  9.  
     
  10.  
    public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
  11.  
     
  12.  
    /**
  13.  
    * add @ApiVersion to controller class.
  14.  
    *
  15.  
    * @param handlerType handlerType
  16.  
    * @return RequestCondition
  17.  
    */
  18.  
    @Override
  19.  
    protected RequestCondition<?> getCustomTypeCondition(@NonNull Class<?> handlerType) {
  20.  
    ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
  21.  
    return null == apiVersion ? super.getCustomTypeCondition(handlerType) : new ApiVersionCondition(apiVersion.value());
  22.  
    }
  23.  
     
  24.  
    /**
  25.  
    * add @ApiVersion to controller method.
  26.  
    *
  27.  
    * @param method method
  28.  
    * @return RequestCondition
  29.  
    */
  30.  
    @Override
  31.  
    protected RequestCondition<?> getCustomMethodCondition(@NonNull Method method) {
  32.  
    ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
  33.  
    return null == apiVersion ? super.getCustomMethodCondition(method) : new ApiVersionCondition(apiVersion.value());
  34.  
    }
  35.  
     
  36.  
    }
  37.  
     
学新通

配置注册HandlerMapping

  1.  
    package tech.pdai.springboot.api.version.config.version;
  2.  
     
  3.  
    import org.springframework.context.annotation.Configuration;
  4.  
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
  5.  
    import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
  6.  
     
  7.  
    @Configuration
  8.  
    public class CustomWebMvcConfiguration extends WebMvcConfigurationSupport {
  9.  
     
  10.  
    @Override
  11.  
    public RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
  12.  
    return new ApiVersionRequestMappingHandlerMapping();
  13.  
    }
  14.  
    }

或者实现WebMvcRegistrations的接口

  1.  
    @Configuration
  2.  
    @RequiredArgsConstructor
  3.  
    public class WebConfig implements WebMvcConfigurer, WebMvcRegistrations {
  4.  
    //...
  5.  
     
  6.  
    @Override
  7.  
    @NonNull
  8.  
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
  9.  
    return new ApiVersionRequestMappingHandlerMapping();
  10.  
    }
  11.  
     
  12.  
    }

测试运行

controller

  1.  
    package tech.pdai.springboot.api.version.controller;
  2.  
     
  3.  
    import org.springframework.web.bind.annotation.RequestMapping;
  4.  
    import org.springframework.web.bind.annotation.RestController;
  5.  
    import tech.pdai.springboot.api.version.config.version.ApiVersion;
  6.  
    import tech.pdai.springboot.api.version.entity.User;
  7.  
     
  8.  
    /**
  9.  
    * @author pdai
  10.  
    */
  11.  
    @RestController
  12.  
    @RequestMapping("api/{v}/user")
  13.  
    public class UserController {
  14.  
     
  15.  
    @RequestMapping("get")
  16.  
    public User getUser() {
  17.  
    return User.builder().age(18).name("pdai, default").build();
  18.  
    }
  19.  
     
  20.  
    @ApiVersion("1.0.0")
  21.  
    @RequestMapping("get")
  22.  
    public User getUserV1() {
  23.  
    return User.builder().age(18).name("pdai, v1.0.0").build();
  24.  
    }
  25.  
     
  26.  
    @ApiVersion("1.1.0")
  27.  
    @RequestMapping("get")
  28.  
    public User getUserV11() {
  29.  
    return User.builder().age(19).name("pdai, v1.1.0").build();
  30.  
    }
  31.  
     
  32.  
    @ApiVersion("1.1.2")
  33.  
    @RequestMapping("get")
  34.  
    public User getUserV112() {
  35.  
    return User.builder().age(19).name("pdai2, v1.1.2").build();
  36.  
    }
  37.  
    }
学新通

输出

  1.  
    http://localhost:8080/api/v1/user/get
  2.  
    // {"name":"pdai, v1.0.0","age":18}
  3.  
     
  4.  
    http://localhost:8080/api/v1.1/user/get
  5.  
    // {"name":"pdai, v1.1.0","age":19}
  6.  
     
  7.  
    http://localhost:8080/api/v1.1.1/user/get
  8.  
    // {"name":"pdai, v1.1.0","age":19} 匹配比1.1.1小的中最大的一个版本号
  9.  
     
  10.  
    http://localhost:8080/api/v1.1.2/user/get
  11.  
    // {"name":"pdai2, v1.1.2","age":19}
  12.  
     
  13.  
    http://localhost:8080/api/v1.2/user/get
  14.  
    // {"name":"pdai2, v1.1.2","age":19} 匹配最大的版本号,v1.1.2
  15.  
     
学新通

这样,如果我们向另外一个模块提供v1版本的接口,新的需求中只变动了一个接口方法,这时候我们只需要增加一个接口添加版本号v1.1即可用v1.1版本访问所有接口。

此外,这种方式可能会导致v3版本接口没有发布,但是是可以通过v3访问接口的;这种情况下可以添加一些限制版本的逻辑,比如最大版本,版本集合等。

示例源码

https://github.dev/realpdai/tech-pdai-spring-demos

 著作权归https://pdai.tech所有。 链接:SpringBoot接口 - 如何提供多个版本接口 | Java 全栈知识体系

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhhafhjb
系列文章
更多 icon
同类精品
更多 icon
继续加载