springmvc--版本控制


SpringMVC Api接口版本控制

SpringMVC Api接口版本控制

1. 問題

​ 后端服務在提供api接口時,隨着業務的變化,原有的接口很有可能不能滿足現有的需求。在無法修改原有接口的情況下,只能提供一個新版本的接口來開放新的業務能力。

​ 區分不同版本的api接口的方式有多種,其中一種簡單通用的方式是在uri中添加版本的標識,例如/api/v1/user,api/v3/user。通過v+版本號來指定不同版本的api接口。在后端服務的代碼中,可以將版本號直接寫入代碼中,例如,user接口提供兩個入口方法,url mapping分別指定為/api/v1/user/api/v2/user

這種方式主要有幾個缺陷:

  1. 通常為了統一控制,調用方會使用統一一個版本來調用接口。如果后端服務在升級接口的版本時,實際只需要變更其中幾個接口的邏輯,其余接口只能通過添加新的mapping來完成升級。
  2. 接口的優先匹配,當調用高版本的api接口時,理論應該訪問當前最高版本的接口,例如,如果當前整體api版本為4,但是實際上/user接口的mapping配置最高版本為v2,這時使用v4或者v2調用/user接口時,都應該返回/v2/user的結果。

2. 解決方式

​ 為了較好地解決上面的問題,需要從SpringMVC對uri映射到接口的邏輯做一個擴展。

2.1 SpringMVC映射請求到處理方法的過程

​ SpringMVC處理請求分發的過程中主要的幾個類為:

HandlerMapping: 定義根據請求獲取處理當期請求的HandlerChain的getHandler方法,其中包括實際處理邏輯的handler對象和攔截器

AbstractHandlerMapping: 實現HandlerMapping接口的抽象類,在getHandler方法實現了攔截器的初始化和handler對象獲取,其中獲取handler對象的getHandlerInternal方法為抽象方法,由子類實現

AbstractHandlerMethodMapping<T>: 繼承AbstractHandlerMapping,定義了method handler映射關系,每一個method handler都一個唯一的T關聯

RequestMappingInfoHandlerMapping: 繼承``AbstractHandlerMethodMapping<RequestMappingInfo>`,定義了RequestMappingInfo與method handler的關聯關系

RequestMappingInfo: 包含各種匹配規則RequestCondition,請求到method的映射規則信息都包含在這個對象中,

Condition 說明
PatternsRequestCondition url匹配規則
RequestMethodsRequestCondition http方法匹配規則,例如GET,POST等
ParamsRequestCondition 參數匹配規則
HeadersRequestCondition http header匹配規則
ConsumesRequestCondition 請求Content-Type頭部的媒體類型匹配規則
ProducesRequestCondition 請求Accept頭部媒體類型匹配規則
RequestCondition 自定義condition

RequestMappingHandlerMapping: 繼承RequestMappingInfoHandlerMapping,處理方法的@ReqeustMapping注解,將其與method handler與@ReqeustMapping注解構建的RequestMappingInfo關聯

2.1.1 Spring初始化RequestMappingInfo與handler的關系

​ Spring在初始化RequestMappingHandlerMappingBean的時候,會初始化Controller的方法與RequestMappingInfo的映射關系並緩存,方便請求過來時,查詢使用。

RequestMappingHandlerMapping實現了InitializingBean接口(父類實現),接口說明如下:

/** * Interface to be implemented by beans that need to react once all their * properties have been set by a BeanFactory: for example, to perform custom * initialization, or merely to check that all mandatory properties have been set. */ public interface InitializingBean { /** * BeanFactory初始化bean的屬性完成后會調用當前方法 */ void afterPropertiesSet() throws Exception; } 

​ 當RequestMappingHandlerMappingBean屬性初始化完成之后,BeanFactory對調用afterPropertiesSet方法:

    @Override public void afterPropertiesSet() { //初始化handler methods initHandlerMethods(); } protected void initHandlerMethods() { ... // 查詢所有bean,分別檢測是否有@Controller和@RequestMapping配置 String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ? BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) : obtainApplicationContext().getBeanNamesForType(Object.class)); for (String beanName : beanNames) { if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { Class<?> beanType = null; try { beanType = obtainApplicationContext().getType(beanName); } catch (Throwable ex) { // An unresolvable bean type, probably from a lazy bean - let's ignore it. if (logger.isDebugEnabled()) { logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex); } } // 只處理注解了@Controller或@RequestMapping,@RestController、@GetMapping等也 // 符合條件 if (beanType != null && isHandler(beanType)) { // 檢測Mapping並注冊 detectHandlerMethods(beanName); } } } // 空實現 handlerMethodsInitialized(getHandlerMethods()); } 

detectHandlerMethods方法會將Controller中所以可以檢測到RequestCondition的方法抽取出來,並將包含RequestCondition集合的對象RequestMappingInfo一起注冊,RequestCondition集合包括所有配置的規則,例如:

@RequestMapping(value = "/test", method = RequestMethod.GET, consumes = MediaType.APPLICATION_JSON_VALUE) 

detectHandlerMethods方法:

    protected void detectHandlerMethods(final Object handler) { // handlerType有可能是beanName,獲取bean實例 Class<?> handlerType有可能是beanName = (handler instanceof String ? obtainApplicationContext().getType((String) handler) : handler.getClass()); if (handlerType != null) { // 獲取實際的Controller Class對象,處理CGLIB代理類的情況,拿到被代理的Class對象 final Class<?> userType = ClassUtils.getUserClass(handlerType); Map<Method, T> methods = MethodIntrospector.selectMethods(userType, (MethodIntrospector.MetadataLookup<T>) method -> { try { // 實際獲取RequestMappingInfo return getMappingForMethod(method, userType); } catch (Throwable ex) { throw new IllegalStateException("Invalid mapping on handler class [" + userType.getName() + "]: " + method, ex); } }); if (logger.isDebugEnabled()) { logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods); } methods.forEach((method, mapping) -> { Method invocableMethod = AopUtils.selectInvocableMethod(method, userType); // 注冊RequestMappingInfo和method的關聯關系 registerHandlerMethod(handler, invocableMethod, mapping); }); } } 

getMappingForMethod方法中,將方法與類中的@RequestMapping注解信息結合,同時獲取用戶自定義的RequestCondition,將所有的condition組合成一個RequestMappingInfo返回,獲取不到則返回null。

    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) { // 獲取方法的RequestMappingInfo,包括自定義的RequestCondition RequestMappingInfo info = createRequestMappingInfo(method); if (info != null) { // 獲取類的RequestMappingInfo,包括自定義的RequestCondition RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType); if (typeInfo != null) { info = typeInfo.combine(info); } } return info; } // 獲取RequestMappingInfo private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) { // 獲取@RequestMapping注解 RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); // 獲取自定義的RequestCondition RequestCondition<?> condition = (element instanceof Class ? getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element)); // 組合自定義的RequestCondition與@RequestMapping的信息返回RequestMappingInfo return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null); } 
2.1.2 Spring根據mapping關系查詢處理請求的方法

​ 主要功能入口在AbstractHandlerMethodMappinglookupHandlerMethod方法,首先根據上一節注冊的@RequestMapping配置的uri直接查詢是否有對應的處理方法,如果查詢不到,例如url配置中有占位符,不能直接匹配上,則遍歷Mapping緩存查詢:

    protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { List<Match> matches = new ArrayList<>(); // 1.直接根據url查詢關聯 List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath); if (directPathMatches != null) { addMatchingMappings(directPathMatches, matches, request); } if (matches.isEmpty()) { // 2.直接根據url查詢不到,遍歷映射緩存,確認是否有匹配的handler方法 addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request); } // 3.如果有多個匹配,根據規則做一個排序,拿最佳匹配的handler,如果無法區分就會報錯 ... } 

​ 在步驟2中,會將緩存中的RequestMappingInfo查詢出來,並對當前HttpServletRequest做一個匹配,主要邏輯是使用RequestMappingInfo中保存的各種RequestCondition匹配當前請求,也包括自定義的RequestCondition,返回匹配結果,主要的方法為RequestMappingInfo的getMatchingCondition

    public RequestMappingInfo getMatchingCondition(HttpServletRequest request) { // 使用當前對象保存的RequestMethodsRequestCondition信息匹配request RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request); // 使用當前對象保存的ParamsRequestCondition信息匹配request ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request); HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request); ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request); ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request); if (methods == null || params == null || headers == null || consumes == null || produces == null) { return null; } PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request); if (patterns == null) { return null; } // 使用當前對象保存的自定義的RequestCondition信息匹配request RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request); if (custom == null) { return null; } // 如果匹配,返回匹配的結果 return new RequestMappingInfo(this.name, patterns, methods, params, headers, consumes, produces, custom.getCondition()); } 

​ 將請求分發到具體的Controller方法的邏輯主要是初始化過程中注冊的Mapping緩存(RequestMappingInfo)查找與匹配的過程,RequestMappingInfo中包含各種RequestCondition,包括參數、HTTP方法、媒體類型等規則的匹配,同時還包含了一個自定義的RequestCondition的擴展,如果想要增加自定義的Request匹配規則,就可以從這里入手。

2.2 自定義RequestCondition實現版本控制

​ RequestCondition定義:

public interface RequestCondition<T> { /** * 同另一個condition組合,例如,方法和類都配置了@RequestMapping的url,可以組合 */ T combine(T other); /** * 檢查request是否匹配,可能會返回新建的對象,例如,如果規則配置了多個模糊規則,可能當前請求 * 只滿足其中幾個,那么只會返回這幾個條件構建的Condition */ @Nullable T getMatchingCondition(HttpServletRequest request); /** * 比較,請求同時滿足多個Condition時,可以區分優先使用哪一個 */ int compareTo(T other, HttpServletRequest request); } 

​ 同@RequestMapping一樣,我們同樣定義一個自定義注解,來保存接口方法的規則信息:

@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ApiVersion { // 定義接口的版本號 int value() default 1; } 

​ 自定義一個新的RequestCondition:

public class ApiVersionRequestCondition implements RequestCondition<ApiVersionRequestCondition> { // 用於匹配request中的版本號 v1 v2 private static final Pattern VERSION_PATTERN = Pattern.compile("/v(\\d+).*"); // 保存當前的版本號 private int version; // 保存所有接口的最大版本號 private static int maxVersion = 1; public ApiVersionRequestCondition(int version) { this.version = version; } @Override public ApiVersionRequestCondition combine(ApiVersionRequestCondition other) { // 上文的getMappingForMethod方法中是使用 類的Condition.combine(方法的condition)的結果 // 確定一個方法的condition,所以偷懶的寫法,直接返回參數的版本,可以保證方法優先,可以優化 // 在condition中增加一個來源於類或者方法的標識,以此判斷,優先整合方法的condition return new ApiVersionRequestCondition(other.version); } @Override public ApiVersionRequestCondition getMatchingCondition(HttpServletRequest request) { // 正則匹配請求的uri,看是否有版本號 v1 Matcher matcher = VERSION_PATTERN.matcher(request.getRequestURI()); if (matcher.find()) { String versionNo = matcher.group(1); int version = Integer.valueOf(versionNo); // 超過當前最大版本號或者低於最低的版本號均返回不匹配 if (version <= maxVersion && version >= this.version) { return this; } } return null; } @Override public int compareTo(ApiVersionRequestCondition other, HttpServletRequest request) { // 以版本號大小判定優先級,越高越優先 return other.version - this.version; } public int getVersion() { return version; } public static void setMaxVersion(int maxVersion) { ApiVersionRequestCondition.maxVersion = maxVersion; } } 

​ 因為默認的RequestMappingHandlerMapping實現只有一個空的獲取自定義RequestCondition的實現,所以需要繼承實現:

public class ApiHandlerMapping extends RequestMappingHandlerMapping { private int latestVersion = 1; @Override protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) { // 判斷是否有@ApiVersion注解,構建基於@ApiVersion的RequestCondition ApiVersionRequestCondition condition = buildFrom(AnnotationUtils.findAnnotation(handlerType, ApiVersion.class)); // 保存最大版本號 if (condition != null && condition.getVersion() > latestVersion) { ApiVersionRequestCondition.setMaxVersion(condition.getVersion()); } return condition; } @Override protected RequestCondition<?> getCustomMethodCondition(Method method) { // 判斷是否有@ApiVersion注解,構建基於@ApiVersion的RequestCondition ApiVersionRequestCondition condition = buildFrom(AnnotationUtils.findAnnotation(method, ApiVersion.class)); // 保存最大版本號 if (condition != null && condition.getVersion() > latestVersion) { ApiVersionRequestCondition.setMaxVersion(condition.getVersion()); } return condition; } private ApiVersionRequestCondition buildFrom(ApiVersion apiVersion) { return apiVersion == null ? null : new ApiVersionRequestCondition(apiVersion.value()); } } 

​ 在SpringBoot項目中增加Config,注入自定義的ApiHandlerMapping:

@Configuration public class Config extends WebMvcConfigurationSupport { @Override public RequestMappingHandlerMapping requestMappingHandlerMapping() { ApiHandlerMapping handlerMapping = new ApiHandlerMapping(); handlerMapping.setOrder(0); handlerMapping.setInterceptors(getInterceptors()); return handlerMapping; } } 

​ 自定義Contoller測試:

@RestController @ApiVersion // 在url中增加一個占位符,用於匹配未知的版本 v1 v2... @RequestMapping("/api/{version}") public class Controller { @GetMapping("/user/{id}") @ApiVersion(2) public Result<User> getUser(@PathVariable("id") String id) { return new Result<>("0", "get user V2 :" + id, new User("user2_" + id, 20)); } @GetMapping("/user/{id}") @ApiVersion(4) public Result<User> getUserV4(@PathVariable("id") String id) { return new Result<>("0", "get user V4 :" + id, new User("user4_" + id, 20)); } @GetMapping("/cat/{id}") public Result<User> getCatV1(@PathVariable("id") String id) { return new Result<>("0", "get cat V1 :" + id, new User("cat1_" + id, 20)); } @GetMapping("/dog/{id}") public Result<User> getDogV1(@PathVariable("id") String id) { return new Result<>("0", "get dog V3 :" + id, new User("dog1_" + id, 20)); } } 
// Result定義 public class Result<T> { private String code; private String msg; private T data; public Result() { } public Result(String code, String msg, T data) { this.code = code; this.msg = msg; this.data = data; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public T getData() { return data; } public void setData(T data) { this.data = data; } }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM