SpringBoot 接口版本控制
一個系統在上線后會不斷迭代更新,需求也會不斷變化,有可能接口的參數也會發生變化,如果在原有的參數上直接修改,可能會影響到現有項目的正常運行,這時我們就需要設置不同的版本,這樣即使參數發生變化,由于老版本沒有變化,因此不會影響上線系統的運行。
這里我們選擇使用帶有一位小數的浮點數作為版本號,在請求地址末尾中帶上版本號,大致的地址如:http://api/test/1.0,其中,1.0即代表的是版本號。具體做法請看代碼
自定義一個版本號的注解接口ApiVersion.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import org.springframework.web.bind.annotation.Mapping; import java.lang.annotation.*; /** * 版本控制 * @author Zac */ @Target ({ElementType.METHOD, ElementType.TYPE}) @Retention (RetentionPolicy.RUNTIME) @Documented @Mapping public @interface ApiVersion { /** * 標識版本號 * @return */ double value(); } |
版本號篩選器ApiVersionCondition
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
|
import org.springframework.web.servlet.mvc.condition.RequestCondition; import javax.servlet.http.HttpServletRequest; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 版本號匹配篩選器 * @author Zac */ public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> { /** * 路徑中版本的正則表達式匹配, 這里用 /1.0的形式 */ private static final Pattern VERSION_PREFIX_PATTERN = Pattern.compile( "^\\S+/([1-9][.][0-9])$" ); private double apiVersion; public ApiVersionCondition( double apiVersion) { this .apiVersion = apiVersion; } @Override public ApiVersionCondition combine(ApiVersionCondition other) { // 采用最后定義優先原則,則方法上的定義覆蓋類上面的定義 return new ApiVersionCondition(other.getApiVersion()); } @Override public ApiVersionCondition getMatchingCondition(HttpServletRequest request) { Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI()); if (m.find()) { Double version = Double.valueOf(m.group( 1 )); // 如果請求的版本號大于配置版本號, 則滿足 if (version >= this .apiVersion) { return this ; } } return null ; } @Override public int compareTo(ApiVersionCondition other, HttpServletRequest request) { // 優先匹配最新的版本號 return Double.compare(other.getApiVersion(), this .apiVersion); } public double getApiVersion() { return apiVersion; } } |
版本號匹配攔截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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; /** * 版本號匹配攔截器 * @author Zac */ public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping { @Override protected RequestCondition<ApiVersionCondition> getCustomTypeCondition(Class<?> handlerType) { ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion. class ); return createCondition(apiVersion); } @Override protected RequestCondition<ApiVersionCondition> getCustomMethodCondition(Method method) { ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion. class ); return createCondition(apiVersion); } private RequestCondition<ApiVersionCondition> createCondition(ApiVersion apiVersion) { return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value()); } } |
配置WebMvcRegistrationsConfig
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.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.web.WebMvcRegistrationsAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; @SpringBootConfiguration public class WebMvcRegistrationsConfig extends WebMvcRegistrationsAdapter { @Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { return new CustomRequestMappingHandlerMapping(); } } controller層實現 /** * version * @return */ @GetMapping ( "/api/test/{version}" ) @ApiVersion ( 2.0 ) public String searchTargetImage() { return "Hello! Welcome to Version2" ; } /** * 多目標類型搜索,關聯圖片查詢接口 * * @param attribute * @return */ @GetMapping ( "/api/test/{version}" ) @ApiVersion ( 1.0 ) public AppResult searchTargetImage() { return "Hello! Welcome to Version1" ; } |
SpringBoot 2.x 接口多版本
準備將現有的接口加上版本管理,兼容以前的版本。網上一調研,發現有很多示例,但是還是存在以下兩個問題。
1.大部分使用Integer作為版本號,但是通常的版本號形式為v1.0.0,
2.版本號攜帶在header中,對接調用不清晰。
針對以上兩個問題,做如下改造。
1.自定義接口版本注解ApiVersion
后面條件映射使用equals匹配,此處是否將String變為String[]應對多個版本使用同一代碼的問題。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package com.yugioh.api.common.core.version; import org.springframework.web.bind.annotation.Mapping; import java.lang.annotation.*; /** * 接口版本 * * @author lieber */ @Target ({ElementType.METHOD, ElementType.TYPE}) @Retention (RetentionPolicy.RUNTIME) @Documented @Mapping public @interface ApiVersion { String value() default "1.0.0" ; } |
2.請求映射條件ApiVersionCondition
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
|
package com.yugioh.api.common.core.version; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.web.servlet.mvc.condition.RequestCondition; import javax.servlet.http.HttpServletRequest; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 版本控制 * * @author lieber */ @AllArgsConstructor @Getter @Slf4j public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> { private String version; private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile( ".*v(\\d+(.\\d+){0,2}).*" ); public final static String API_VERSION_CONDITION_NULL_KEY = "API_VERSION_CONDITION_NULL_KEY" ; @Override public ApiVersionCondition combine(ApiVersionCondition other) { // 方法上的注解優于類上的注解 return new ApiVersionCondition(other.getVersion()); } @Override public ApiVersionCondition getMatchingCondition(HttpServletRequest request) { Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI()); if (m.find()) { String version = m.group( 1 ); if ( this .compareTo(version)) { return this ; } } // 將錯誤放在request中,可以在錯誤頁面明確提示,此處可重構為拋出運行時異常 request.setAttribute(API_VERSION_CONDITION_NULL_KEY, true ); return null ; } @Override public int compareTo(ApiVersionCondition other, HttpServletRequest request) { return this .compareTo(other.getVersion()) ? 1 : - 1 ; } private boolean compareTo(String version) { return Objects.equals(version, this .version); } } |
3.創建自定義匹配處理器ApiVersionRequestMappingHandlerMapping
網上大部分只重寫了getCustomTypeCondition和getCustomMethodCondition方法。這里為了解決路由映射問題,重寫getMappingForMethod方法,在路由中加入前綴{version},加入后路由變為/api/{version}/xxx
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
|
package com.yugioh.api.common.core.version; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.web.servlet.mvc.condition.RequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import java.lang.reflect.Method; /** * 自定義匹配處理器 * * @author lieber */ public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping { private final static String VERSION_PREFIX = "{version}" ; @Override protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) { ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion. class ); return createCondition(apiVersion); } @Override protected RequestCondition<?> getCustomMethodCondition(Method method) { ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion. class ); return createCondition(apiVersion); } @Override protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) { RequestMappingInfo requestMappingInfo = super .getMappingForMethod(method, handlerType); if (requestMappingInfo == null ) { return null ; } return createCustomRequestMappingInfo(method, handlerType, requestMappingInfo); } private RequestMappingInfo createCustomRequestMappingInfo(Method method, Class<?> handlerType, RequestMappingInfo requestMappingInfo) { ApiVersion methodApi = AnnotatedElementUtils.findMergedAnnotation(method, ApiVersion. class ); ApiVersion handlerApi = AnnotatedElementUtils.findMergedAnnotation(handlerType, ApiVersion. class ); if (methodApi != null || handlerApi != null ) { return RequestMappingInfo.paths(VERSION_PREFIX).options( this .config).build().combine(requestMappingInfo); } return requestMappingInfo; } private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration(); private RequestCondition<ApiVersionCondition> createCondition(ApiVersion apiVersion) { return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value()); } } |
4.使用ApiVersionConfig配置來決定是否開啟多版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package com.yugioh.api.common.core.version; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; /** * 版本管理器配置 * * @author lieber */ @Data @Configuration @ConfigurationProperties (prefix = "api.config.version" , ignoreInvalidFields = true ) public class ApiVersionConfig { /** * 是否開啟 */ private boolean enable; } |
5.配置WebMvcRegistrations,啟用自定義路由Mapping
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
|
package com.yugioh.api.common.core.version; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; /** * @author lieber */ @Configuration @Order (Ordered.HIGHEST_PRECEDENCE) public class ApiWebMvcRegistrations implements WebMvcRegistrations { private final ApiVersionConfig apiVersionConfig; @Autowired public ApiWebMvcRegistrations(ApiVersionConfig apiVersionConfig) { this .apiVersionConfig = apiVersionConfig; } @Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { if (apiVersionConfig.isEnable()) { return new ApiVersionRequestMappingHandlerMapping(); } return null ; } } |
至此我們就能在項目中愉快的使用@APIVersion來指定版本了,但是現在這個和swagger整合還會有問題,繼續研究中…
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持服務器之家。
原文鏈接:https://blog.csdn.net/asd120829243/article/details/90175930