SpringBoot 增加 API Version
基于restful風格上,增加version版本號
例如: get /api/v1/users/
一、增加ApiVersion自定義注解
作用于Controller上,指定API版本號
這里版本號使用了double ,考慮到小版本的情況,例如1.1
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import java.lang.annotation.*; /** * API Version type */ @Target ({ElementType.METHOD, ElementType.TYPE}) @Retention (RetentionPolicy.RUNTIME) @Documented public @interface ApiVersion { /** * api version begin 1 */ double version() default 1 ; } |
二、新增RequestCondition自定義匹配條件
Spring提供RequestCondition接口,用于定義API匹配條件
這里通過自定義匹配條件,識別ApiVersion,進行版本匹配
getMatchingCondition 用于檢查URL中,是否符合/v{版本號},用于過濾無版本號接口;
compareTo 用于決定多個相同API時,使用哪個接口進行處理;
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
|
import org.springframework.web.servlet.mvc.condition.RequestCondition; import javax.servlet.http.HttpServletRequest; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * API version condition * @author w * @date 2020-11-16 */ public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> { /** * 接口路徑中的版本號前綴,如: api/v[1-n]/test */ private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile( "/v([0-9]+\\.{0,1}[0-9]{0,2})/" ); /** API VERSION interface **/ private ApiVersion apiVersion; ApiVersionCondition(ApiVersion apiVersion){ this .apiVersion = apiVersion; } /** * [當class 和 method 請求url相同時,觸發(fā)此方法用于合并url] * 官方解釋: * - 某個接口有多個規(guī)則時,進行合并 * - 比如類上指定了@RequestMapping的 url 為 root * - 而方法上指定的@RequestMapping的 url 為 method * - 那么在獲取這個接口的 url 匹配規(guī)則時,類上掃描一次,方法上掃描一次,這個時候就需要把這兩個合并成一個,表示這個接口匹配root/method * @param other 相同api version condition * @return ApiVersionCondition */ @Override public ApiVersionCondition combine(ApiVersionCondition other) { // 此處按優(yōu)先級,method大于class return new ApiVersionCondition(other.getApiVersion()); } /** * 判斷是否成功,失敗返回 null;否則,則返回匹配成功的條件 * @param httpServletRequest http request * @return 匹配成功條件 */ @Override public ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) { // 通過uri匹配版本號 System.out.println(httpServletRequest.getRequestURI()); Matcher m = VERSION_PREFIX_PATTERN.matcher(httpServletRequest.getRequestURI()); if (m.find()) { // 獲得符合匹配條件的ApiVersionCondition System.out.println( "groupCount:" +m.groupCount()); double version = Double.valueOf(m.group( 1 )); if (version >= getApiVersion().version()) { return this ; } } return null ; } /** * 多個都滿足條件時,用來指定具體選擇哪一個 * @param other 多個時 * @param httpServletRequest http request * @return 取版本號最大的 */ @Override public int compareTo(ApiVersionCondition other, HttpServletRequest httpServletRequest) { // 當出現多個符合匹配條件的ApiVersionCondition,優(yōu)先匹配版本號較大的 return other.getApiVersion().version() >= getApiVersion().version() ? 1 : - 1 ; } public ApiVersion getApiVersion() { return apiVersion; } } |
三、重寫RequestMappingHandlerMapping處理
通過重寫 RequestMappingHandlerMapping 類,對RequestMappering進行識別@ApiVersion注解,針對性處理;
這里考慮到有些接口不存在版本號,則使用Spring原來的ApiVersionRequestMappingHandlerMapping繼續(xù)處理;
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
|
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.web.servlet.mvc.condition.RequestCondition; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import java.lang.reflect.Method; /** * API version setting * @author w * @date 2020-11-15 */ public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping { /** * class condition * - 在class上加@ApiVersion注解&url加{version} * @param handlerType class type * @return ApiVersionCondition */ @Override protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) { ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType,ApiVersion. class ); return null == apiVersion ? super .getCustomTypeCondition(handlerType) : new ApiVersionCondition(apiVersion); } /** * method condition * - 在方法上加@ApiVersion注解&url加{version} * @param method method object * @return ApiVersionCondition */ @Override protected RequestCondition<?> getCustomMethodCondition(Method method) { ApiVersion apiVersion = AnnotationUtils.findAnnotation(method,ApiVersion. class ); return null == apiVersion ? super .getCustomMethodCondition(method) : new ApiVersionCondition(apiVersion); } } |
四、Controller接口增加@ApiVersion注解
通過@ApiVersion注解指定該接口版本號
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
|
import com.panda.common.web.controller.BasicController; import com.panda.common.web.version.ApiVersion; import com.panda.core.umc.service.UserInfoService; import com.panda.core.umc.vo.QueryUsersConditionVo; import com.panda.face.umc.dto.user.QueryUsersReq; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; /** * 用戶信息服務 * @author w * @date 2020-11-06 */ @RequestMapping ( "/api" ) @RestController public class UserInfoController extends BasicController{ @Autowired private UserInfoService userInfoService; /** * 查詢所有用戶信息 * @param req 查詢條件信息 */ @ApiVersion @RequestMapping (value = "{version}/users" , method = RequestMethod.GET) @ResponseBody public ResponseEntity getUsers( @PathVariable ( "version" ) String version, QueryUsersReq req){ QueryUsersConditionVo condition = new QueryUsersConditionVo(); BeanUtils.copyProperties(req,condition); condition.setOrderBy( "CREATE_TIME" ); condition.setSort( "DESC" ); return assemble( "1111" ); } /** * 查詢所有用戶信息 * @param req 查詢條件信息 */ @ApiVersion (version = 1.1 ) @RequestMapping (value = "{version}/users" , method = RequestMethod.GET) @ResponseBody public ResponseEntity getUsersV2( @PathVariable ( "version" ) String version, QueryUsersReq req){ QueryUsersConditionVo condition = new QueryUsersConditionVo(); BeanUtils.copyProperties(req,condition); condition.setOrderBy( "CREATE_TIME" ); condition.setSort( "DESC" ); return assemble( "222" ); } /** * 根據用戶ID獲取用戶信息 * @param userId 用戶ID */ @RequestMapping (value = "/users/uid/{userId}" , method = RequestMethod.GET) @ResponseBody public ResponseEntity getUserInfo( @PathVariable ( "userId" ) String userId){ return assemble(userInfoService.selectByUserId(userId)); } } |
五、測試調用
通過訪問以下URL,測試返回結果
GET http://127.0.0.1/api/v1/users
GET http://127.0.0.1/api/v1.1/users
GET http://127.0.0.1/api/v1.2/users
GET http://127.0.0.1/api/users/uid/U0001
六、總結
1.通過@ApiVersion注解方式,可以靈活指定接口版本;
2.缺點很明顯,需要在URL上加入{version},才能進行匹配成功,這種PathVariable識別過于模糊,后期排查問題增加困難;
3.建議通過包名增加v1/v2明顯區(qū)分版本,且在controller的URL上直接寫死v1版本號,這種更直觀;
SpringBoot的項目API版本控制
一、自定義版本號標記注解
1
2
3
4
5
6
7
8
9
|
@Target ({ElementType.METHOD, ElementType.TYPE}) @Retention (RetentionPolicy.RUNTIME) @Documented public @interface ApiVersion { /** * 標識版本號,從1開始 */ int value() default 1 ; } |
二、重寫RequestCondition,自定義url匹配邏輯
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
|
@Data @Slf4j public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> { /** * 接口路徑中的版本號前綴,如: api/v[1-n]/fun */ private final static Pattern VERSION_PREFIX = Pattern.compile( "/v(\\d+)/" ); private int apiVersion; ApiVersionCondition( int apiVersion) { this .apiVersion = apiVersion; } /** * 最近優(yōu)先原則,方法定義的 @ApiVersion > 類定義的 @ApiVersion */ @Override public ApiVersionCondition combine(ApiVersionCondition other) { return new ApiVersionCondition(other.getApiVersion()); } /** * 獲得符合匹配條件的ApiVersionCondition */ @Override public ApiVersionCondition getMatchingCondition(HttpServletRequest request) { Matcher m = VERSION_PREFIX.matcher(request.getRequestURI()); if (m.find()) { int version = Integer.valueOf(m.group( 1 )); if (version >= getApiVersion()) { return this ; } } return null ; } /** * 當出現多個符合匹配條件的ApiVersionCondition,優(yōu)先匹配版本號較大的 */ @Override public int compareTo(ApiVersionCondition other, HttpServletRequest request) { return other.getApiVersion() - getApiVersion(); } } |
說明:
getMatchingCondition方法中,控制了只有版本小于等于請求參數中的版本的 ApiCondition 才滿足規(guī)則
compareTo 指定了當有多個ApiCoondition滿足這個請求時,選擇最大的版本
三、重寫RequestMappingHandlerMapping,自定義匹配的處理器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class ApiRequestMappingHandlerMapping extends RequestMappingHandlerMapping { @Override protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) { // 掃描類上的 @ApiVersion ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion. class ); return createRequestCondition(apiVersion); } @Override protected RequestCondition<?> getCustomMethodCondition(Method method) { // 掃描方法上的 @ApiVersion ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion. class ); return createRequestCondition(apiVersion); } private RequestCondition<ApiVersionCondition> createRequestCondition(ApiVersion apiVersion) { if (Objects.isNull(apiVersion)) { return null ; } int value = apiVersion.value(); Assert.isTrue(value >= 1 , "Api Version Must be greater than or equal to 1" ); return new ApiVersionCondition(value); } } |
四、配置注冊自定義WebMvcRegistrations
1
2
3
4
5
6
7
|
@Configuration public class WebMvcRegistrationsConfig implements WebMvcRegistrations { @Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { return new ApiRequestMappingHandlerMapping(); } } |
五、編寫測試接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@RestController @RequestMapping ( "/api/{version}" ) public class ApiControler { @GetMapping ( "/fun" ) public String fun1() { return "fun 1" ; } @ApiVersion ( 5 ) @GetMapping ( "/fun" ) public String fun2() { return "fun 2" ; } @ApiVersion ( 9 ) @GetMapping ( "/fun" ) public String fun3() { return "fun 5" ; } } |
頁面測試效果:
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持服務器之家。
原文鏈接:https://blog.csdn.net/bluewait321/article/details/110192577