SpringBoot API增加version版本號方式
SpringBoot 增加 API Version
基於restful風格上,增加version版本號
例如: get /api/v1/users/
一、增加ApiVersion自定義註解
作用於Controller上,指定API版本號
這裡版本號使用瞭double ,考慮到小版本的情況,例如1.1
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時,使用哪個接口進行處理;
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相同時,觸發此方法用於合並url] * 官方解釋: * - 某個接口有多個規則時,進行合並 * - 比如類上指定瞭@RequestMapping的 url 為 root * - 而方法上指定的@RequestMapping的 url 為 method * - 那麼在獲取這個接口的 url 匹配規則時,類上掃描一次,方法上掃描一次,這個時候就需要把這兩個合並成一個,表示這個接口匹配root/method * @param other 相同api version condition * @return ApiVersionCondition */ @Override public ApiVersionCondition combine(ApiVersionCondition other) { // 此處按優先級,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,優先匹配版本號較大的 return other.getApiVersion().version() >= getApiVersion().version() ? 1 : -1; } public ApiVersion getApiVersion() { return apiVersion; } }
三、重寫RequestMappingHandlerMapping處理
通過重寫 RequestMappingHandlerMapping 類,對RequestMappering進行識別@ApiVersion註解,針對性處理;
這裡考慮到有些接口不存在版本號,則使用Spring原來的ApiVersionRequestMappingHandlerMapping繼續處理;
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註解指定該接口版本號
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明顯區分版本,且在controller的URL上直接寫死v1版本號,這種更直觀;
SpringBoot的項目API版本控制
一、自定義版本號標記註解
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ApiVersion { /** * 標識版本號,從1開始 */ int value() default 1; }
二、重寫RequestCondition,自定義url匹配邏輯
@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; } /** * 最近優先原則,方法定義的 @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,優先匹配版本號較大的 */ @Override public int compareTo(ApiVersionCondition other, HttpServletRequest request) { return other.getApiVersion() - getApiVersion(); } }
說明:
getMatchingCondition方法中,控制瞭隻有版本小於等於請求參數中的版本的 ApiCondition 才滿足規則
compareTo 指定瞭當有多個ApiCoondition滿足這個請求時,選擇最大的版本
三、重寫RequestMappingHandlerMapping,自定義匹配的處理器
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
@Configuration public class WebMvcRegistrationsConfig implements WebMvcRegistrations { @Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { return new ApiRequestMappingHandlerMapping(); } }
五、編寫測試接口
@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"; } }
頁面測試效果:
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- SpringBoot如何實現接口版本控制
- Java工具類之@RequestMapping註解
- SpringBoot根據目錄結構自動生成路由前綴的實現代碼
- SpringMVC攔截器詳解
- SpringBoot根據目錄結構自動配置Url前綴方式