Springboot 提供多个版本接口

1. 接口出现多个版本的原因

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

1.1 控制接口多版本的方式

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

    • api.baidu.com/user?version=v1 表示V1版本的接口,保持原来接口不动

    • api.baidu.com/user?version=v2 表示V2版本的接口,更新新的接口

  • 区分不同的接口域名,不同的版本有不同的子域名,路由到不同的实例

    • v1.api.baidu.com/user 表示 v1版本的接口, 保持原有接口不动, 路由到instance1

    • v2.api.baidu.com/user 表示 v2版本的接口, 更新新的接口, 路由到instance2

  • 网关路由不同子目录到不同的实例(不同package也可以)

    • api.baiduu.com/v1/user 表示 v1版本的接口, 保持原有接口不动, 路由到instance1

    • api.baiduu.com/v2/user 表示 v2版本的接口, 更新新的接口, 路由到instance2

  • 同一实例,用注解隔离不同版本控制

    • api.baidu.com/v1/user 表示 v1版本的接口, 保持原有接口不动,匹配@ApiVersion(“1”)的handlerMapping

    • api.baidu.com/v2/user 表示 v2版本的接口, 更新新的接口,匹配@ApiVersion(“2”)的handlerMapping

2. 实现案例

这里主要演示的是第四种单一实例中控制接口的版本,基于Springboot封装了@ApiVersion注解方式控制好接口版本

2.1 自定义注解@ApiVersion

1
2
3
4
5
6
7
8
9
10
11
/**
* @author xiaoyuge
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {

String value();
}

2.2 定义版本匹配RequestCondition

版本匹配支持三层版本

  • v1.1.1(大版本,小版本,补丁版本)

  • v1.1 (等同于v1.1.0)

  • v1 (等同于v1.0.0)

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
75
76
/**
* @author xiaoyuge
*/
@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;
}
}

2.3 定义HandlerMapping

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
/**
* @author xiaoyuge
*/
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());
}
}

2.4 注册HandlerMapping

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 注册HandlerMapping
*
* @author xiaoyuge
*/
@Configuration
public class CustomWebMvcConfiguration extends WebMvcConfigurationSupport {
@Override
protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
return new ApiVersionRequestMappingHandlerMapping();
}
}

或者实现WebMvcRegistrations接口

1
2
3
4
5
6
7
8
9
10
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer, WebMvcRegistrations {

@Override
@NonNull
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new ApiVersionRequestMappingHandlerMapping();
}
}

2.5 测试运行

新建测试对象:

1
2
3
4
5
6
7
@Data
@Builder
public class User {

private String name;
private int age;
}

测试方法:

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
/**
* 测试
*
* @author xiaoyuge
*/
@RestController
@RequestMapping("api/{v}/user")
public class UserController {

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

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

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

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

测试结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
http://localhost:8080/api/v1/user/get
// {"name":"xiaoyuge, v1.0.0","age":18}

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

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

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

http://localhost:8080/api/v1.2/user/get
// {"name":"xiaoyuge2, v1.1.2","age":19} 匹配最大的版本号,v1.1.2




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

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