SpringBoot 接口多版本

2024/1/28 SpringBoot 框架

# 1、接口多版本的必要性

# 1.1、多版本接口介绍

开发项目中,随着需求的变更或增加,API接口也会跟着变化,为了不影响原有的API功能,已使用的接口肯定不能直接覆盖更新,需要新增升级版本API与应用版本对应,因此需要多个版本的接口。

# 1.2、接口规范

对于多版本的接口根据不同的需要可以有如下这些规范。

  • version区分
xygalaxy/user?version=v1
xygalaxy/user?version=v2
1
2
  • 路径区分
xygalaxy/v1/user
xygalaxy/v2/user
1
2
  • 域名区分
v1.xygalaxy.com/user
v2.xygalaxy.com/user
1
2

后端处理的话,可以通过判断请求的URL+注解@ApiVersion("1")方式来进行区分。

# 1.3、版本定义

根据常见的三段式版本设计,版本格式定义如下

x.x.x
1

  • 其中第一个 x:对应的是大版本,一般来说只有较大的改动升级,才会改变
  • 其中第二个 x:表示正常的业务迭代版本号,每发布一个常规的 app 升级,这个数值+1
  • 最后一个 x:主要针对 bugfix,比如发布了一个 app,结果发生了异常,需要一个紧急修复,需要再发布一个版本,这个时候可以将这个数值+1

# 2、实现案例

# 2.1、自定义@ApiVersion注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
    String value();
}
1
2
3
4
5
6
7

# 2.2、自定义RequestCondition

版本匹配支持三层版本

v1.1.1 (大版本.小版本.补丁版本) v1.1 (等同于v1.1.0) v1 (等同于v1.0.0)

@Slf4j
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {

    /**
     * support v1.1.1, v1.1, v1; three levels .
     */
    private static final Pattern VERSION_PREFIX_PATTERN_1 = Pattern.compile("/v\\d\\.\\d\\.\\d/");
    private static final Pattern VERSION_PREFIX_PATTERN_2 = Pattern.compile("/v\\d\\.\\d/");
    private static final Pattern VERSION_PREFIX_PATTERN_3 = Pattern.compile("/v\\d/");
    private static final List<Pattern> VERSION_LIST = Collections.unmodifiableList(
            Arrays.asList(VERSION_PREFIX_PATTERN_1, VERSION_PREFIX_PATTERN_2, VERSION_PREFIX_PATTERN_3)
    );

    @Getter
    private final String apiVersion;

    public ApiVersionCondition(String apiVersion) {
        this.apiVersion = apiVersion;
    }

    /**
     * method priority is higher then class.
     *
     * @param other other
     * @return ApiVersionCondition
     */
    @Override
    public ApiVersionCondition combine(ApiVersionCondition other) {
        return new ApiVersionCondition(other.apiVersion);
    }

    @Override
    public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
        for (int vIndex = 0; vIndex < VERSION_LIST.size(); vIndex++) {
            Matcher m = VERSION_LIST.get(vIndex).matcher(request.getRequestURI());
            if (m.find()) {
                String version = m.group(0).replace("/v", "").replace("/", "");
                if (vIndex == 1) {
                    version = version + ".0";
                } else if (vIndex == 2) {
                    version = version + ".0.0";
                }
                if (compareVersion(version, this.apiVersion) >= 0) {
                    log.info("version={}, apiVersion={}", version, this.apiVersion);
                    return this;
                }
            }
        }
        return null;
    }

    @Override
    public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
        return compareVersion(other.getApiVersion(), this.apiVersion);
    }

    private int compareVersion(String version1, String version2) {
        if (version1 == null || version2 == null) {
            throw new RuntimeException("compareVersion error:illegal params.");
        }
        String[] versionArray1 = version1.split("\\.");
        String[] versionArray2 = version2.split("\\.");
        int idx = 0;
        int minLength = Math.min(versionArray1.length, versionArray2.length);
        int diff = 0;
        while (idx < minLength
                && (diff = versionArray1[idx].length() - versionArray2[idx].length()) == 0
                && (diff = versionArray1[idx].compareTo(versionArray2[idx])) == 0) {
            ++idx;
        }
        diff = (diff != 0) ? diff : versionArray1.length - versionArray2.length;
        return diff;
    }
}
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
68
69
70
71
72
73
74

# 2.3、定义HandlerMapping

public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    /**
     * add @ApiVersion to controller class.
     *
     * @param handlerType handlerType
     * @return RequestCondition
     */
    @Override
    protected RequestCondition<?> getCustomTypeCondition(@NonNull Class<?> handlerType) {
        ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
        return null == apiVersion ? super.getCustomTypeCondition(handlerType) : new ApiVersionCondition(apiVersion.value());
    }

    /**
     * add @ApiVersion to controller method.
     *
     * @param method method
     * @return RequestCondition
     */
    @Override
    protected RequestCondition<?> getCustomMethodCondition(@NonNull Method method) {
        ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
        return null == apiVersion ? super.getCustomMethodCondition(method) : new ApiVersionCondition(apiVersion.value());
    }

}
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.4、配置注册HandlerMapping

@Configuration
public class CustomWebMvcConfiguration extends WebMvcConfigurationSupport {

    @Override
    public RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
        return new ApiVersionRequestMappingHandlerMapping();
    }
}
1
2
3
4
5
6
7
8

# 2.5、测试运行结果

@RestController
@RequestMapping("api/{v}/user")
public class UserController {

    @RequestMapping("get")
    public User getUser() {
        return User.builder().age(18).name("xygalaxy, default").build();
    }

    @ApiVersion("1.0.0")
    @RequestMapping("get")
    public User getUserV1() {
        return User.builder().age(18).name("xygalaxy, v1.0.0").build();
    }

    @ApiVersion("1.1.0")
    @RequestMapping("get")
    public User getUserV11() {
        return User.builder().age(19).name("xygalaxy, v1.1.0").build();
    }

    @ApiVersion("1.1.2")
    @RequestMapping("get")
    public User getUserV112() {
        return User.builder().age(19).name("xygalaxy2, v1.1.2").build();
    }
}

/** 测试结果访问

http://localhost:8080/api/v1/user/get
// {"name":"xygalaxy, v1.0.0","age":18}

http://localhost:8080/api/v1.1/user/get
// {"name":"xygalaxy, v1.1.0","age":19}

http://localhost:8080/api/v1.1.1/user/get
// {"name":"xygalaxy, v1.1.0","age":19} 匹配比1.1.1小的中最大的一个版本号

http://localhost:8080/api/v1.1.2/user/get
// {"name":"xygalaxy2, v1.1.2","age":19}

http://localhost:8080/api/v1.2/user/get
// {"name":"xygalaxy2, v1.1.2","age":19} 匹配最大的版本号,v1.1.2
**/
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

参考:SpringBoot接口 - 如何提供多个版本接口 (opens new window)