前面介紹了Spring Boot 如何快速實現Restful api 介面,並以人員資訊為例,設計了一套操作人員資訊的介面。不清楚的可以看之前的文章:https://www.cnblogs.com/zhangweizhong/category/1657780.html。
有些人可能會問,為什麼我看到很多公司的api介面文件裡面,都有/api/v1/ 這樣的地址呢?其實,/api 就是為了和一般的業務地址區分,標明這個地址是api 的介面。v1 則代表版本號。
可能很多人又會問了,為什麼要版本號呢?那麼,接下來就聊一聊Restful 介面為什麼要加版本號? 如何優雅的設計 Restful API 介面版本號?
一、為什麼加版本號
一般來說,api 介面是提供給其他系統或是其他公司使用,不能隨意頻繁的變更。然而,需求和業務不斷變化,介面和引數也會發生相應的變化。如果直接對原來的介面進行修改,勢必會影響線其他系統的正常執行。這就必須對api 介面進行有效的版本控制。
例如,新增使用者的介面,由於業務需求變化,介面的欄位屬性也發生了變化而且可能和之前的功能不相容。為了保證原有的介面呼叫方不受影響,只能重新定義一個新的介面。
Api 版本控制的方式:
1、域名區分管理,即不同的版本使用不同的域名,v1.api.test.com,v2.api.test.com
2、請求url 路徑區分,在同一個域名下使用不同的url路徑,test.com/api/v1/,test.com/api/v2
3、請求引數區分,在同一url路徑下,增加version=v1或v2 等,然後根據不同的版本,選擇執行不同的方法。
實際專案中,一般選擇第二種:請求url路徑區分。因為第二種既能保證水平擴充套件,有不影響以前的老版本。
二、Spring Boot如何實現
實現方案:
1、首先建立自定義的@APIVersion 註解和自定義URL匹配規則ApiVersionCondition。
2、然後建立自定義的 RequestMappingHandlerMapping 匹配對應的request,選擇符合條件的method handler。
1、建立自定義註解
首先,在com.weiz.config 包下,建立一個自定義版本號標記註解 @ApiVersion。
package com.weiz.config; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * API版本控制註解 */ @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface ApiVersion { /** * @return 版本號 */ int value() default 1; }
說明:
ApiVersion 為自定義的註解,API版本控制,返回對應的版本號。
2、自定義url匹配邏輯
建立 ApiVersionCondition 類,並繼承RequestCondition 介面,作用是:版本號篩選,將提取請求URL中版本號,與註解上定義的版本號進行比對,以此來判斷某個請求應落在哪個controller上。
在com.weiz.config 包下建立ApiVersionCondition 類,重寫 RequestCondition,建立自定義的url匹配邏輯。
package com.weiz.config; import org.springframework.web.servlet.mvc.condition.RequestCondition; import javax.servlet.http.HttpServletRequest; import java.util.regex.Matcher; import java.util.regex.Pattern; public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> { private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile(".*v(\\d+).*"); private int apiVersion; ApiVersionCondition(int apiVersion) { this.apiVersion = apiVersion; } private int getApiVersion() { return apiVersion; } @Override public ApiVersionCondition combine(ApiVersionCondition apiVersionCondition) { return new ApiVersionCondition(apiVersionCondition.getApiVersion()); } @Override public ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) { Matcher m = VERSION_PREFIX_PATTERN.matcher(httpServletRequest.getRequestURI()); if (m.find()) { Integer version = Integer.valueOf(m.group(1)); if (version >= this.apiVersion) { return this; } } return null; } @Override public int compareTo(ApiVersionCondition apiVersionCondition, HttpServletRequest httpServletRequest) { return apiVersionCondition.getApiVersion() - this.apiVersion; } }
當方法級別和類級別都有ApiVersion註解時,二者將進行合併(ApiVersionRequestCondition.combine)。最終將提取請求URL中版本號,與註解上定義的版本號進行比對,判斷url是否符合版本要求。
3、自定義匹配的處理器
在com.weiz.config 包下建立 ApiRequestMappingHandlerMapping 類,重寫部分 RequestMappingHandlerMapping 的方法。
package com.weiz.config; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.mvc.condition.RequestCondition; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import java.lang.reflect.Method; public class ApiRequestMappingHandlerMapping extends RequestMappingHandlerMapping { private static final String VERSION_FLAG = "{version}"; private static RequestCondition<ApiVersionCondition> createCondition(Class<?> clazz) { RequestMapping classRequestMapping = clazz.getAnnotation(RequestMapping.class); if (classRequestMapping == null) { return null; } StringBuilder mappingUrlBuilder = new StringBuilder(); if (classRequestMapping.value().length > 0) { mappingUrlBuilder.append(classRequestMapping.value()[0]); } String mappingUrl = mappingUrlBuilder.toString(); if (!mappingUrl.contains(VERSION_FLAG)) { return null; } ApiVersion apiVersion = clazz.getAnnotation(ApiVersion.class); return apiVersion == null ? new ApiVersionCondition(1) : new ApiVersionCondition(apiVersion.value()); } @Override protected RequestCondition<?> getCustomMethodCondition(Method method) { return createCondition(method.getClass()); } @Override protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) { return createCondition(handlerType); } }
4、配置註冊自定義的RequestMappingHandlerMapping
重寫請求過處理的方法,將之前建立的 ApiRequestMappingHandlerMapping 註冊到系統中。
package com.weiz.config; import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; @Configuration public class WebMvcRegistrationsConfig implements WebMvcRegistrations { @Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { return new ApiRequestMappingHandlerMapping(); } }
上面四步,把api 版本控制配置完了。程式碼看著複雜,其實都是重寫spring boot 內部的處理流程。
測試
配置完成之後,接下來編寫測試的控制器進行測試。
1、在Controller/api 目錄下,分別建立UserV1Controller 和 UserV2Controller
UserV1Controller
@RequestMapping("api/{version}/user") @RestController public class UserV1Controller { @GetMapping("/test") public String test() { return "version1"; } @GetMapping("/extend") public String extendTest() { return "user v1 extend"; } }
UserV2Controller
@RequestMapping("api/{version}/user") @RestController @ApiVersion(2) public class UserV2Controller { @GetMapping("/test") public String test() { return "user v2 test"; } }
2、啟動專案後,輸入相關地址,檢視版本控制是否生效
測試結果:
正確的介面地址
繼承的介面地址
說明:
上圖的前兩個截圖說明,請求正確的版本地址,會自動匹配版本的對應介面。當請求的版本大於當前版本時,預設匹配當前版本。
最後
以上,就把Spring Boot 如何優雅的設計 Restful API 介面版本號,實現 API 版本控制介紹完了。版本控制和許可權驗證是rest api 的基礎,雖然看著比較複雜,但是理解了,要實現還是比較簡單的。
這個系列課程的完整原始碼,也會提供給大家。大家關注我的微信公眾號(架構師精進),回覆:springboot原始碼。獲取這個系列課程的完整原始碼。