需求
移動互聯網時代的到來,軟件開發的模式也在變化。記得之前作B/S的後臺開發,基本上沒有Http接口一說,所有是經過渲染模板技術(jsp,freemark)把最終html展現給最終用戶。如今徹底變了,基於後臺接口提供方,咱們歷來不是針對只是瀏覽器展現的後臺輸出,而是各類終端,好比android,ios。因此設計接口的時候必定要當心,一旦放出去的接口可能就永遠都難以變更(除非你強制客戶端用戶升級)。咱們知道,Restful API已經成爲接口設計的一個業務準則。若是你還不是很清楚什麼是Restful,推薦你看一下這篇文章: RESTful API 設計指南。其實,咱們就是設計一套基於http協議的業務接口,可是隨着時間變遷,業務的變化,或者咱們協議自己的優化,都有可能要改變以前存在的接口。這時候給全部接口進行版本管理就顯得很重要了,好比某個添加用戶的接口,因爲業務發展很大,接口的字段屬性變化很大,只能從新定義一個新的接口,由 /v1/user/add 變成了 /v2/user/add,這樣咱們就要維護兩套接口的邏輯,映射到代碼裏,就是要維護兩個不一樣的業務方法。因此這篇文章主要講的是基於SpringMVC開發的應用,怎麼經過擴展開發來方便咱們在代碼層級管理各不一樣的版本接口。html
SpringMVC原理概述
SpringMVC核心思想就是經過一個servlet(DispatchServlet)把請求轉發到各個執行方法上(Controller的method),截張官方的圖以下:android

就是把某個形式的URL(固然,url不是惟一的決定條件,還有好比請求方法,get仍是post,請求頭中的信息)映射到某個類的具體方法上,這個核心的組件在SpringMVC中叫作: HandlerMapping。咱們通常在spring的config文件中作以下配置時會自動初始化加載一個HanlderMapping的實現類:RequestMappingHandlerMapping:ios
1git |
<mvc:annotation-driven/> github |
至於這個一行的配置幹了什麼,能夠從org.springframework.web.servlet.config.MvcNamespaceHandler這個類開始看進去。咱們如今來定義一個Controller,以下:web
1spring 2api 3瀏覽器 4緩存 5 6 7 8 9 |
@Controller public class HelloController { @RequestMapping ( "hello/" ) @ResponseBody public String hello(HttpServletRequest request){ System.out.println( "haha1.........." ); return "hello" ; } } |
這樣咱們經過 /hello/ 就能夠調用了。如今假如咱們針對這個接口的業務出現了很大的變化(涉及到字段,報文的改變,和以前的不能兼容),可是老的接口又不能廢棄,由於你不能保證放出去的接口沒有人調用。因此咱們只能把代碼改爲以下支持多個版本接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@Controller public class HelloController { @RequestMapping ( "v1/hello/" ) @ResponseBody public String hello1(HttpServletRequest request){ System.out.println( "haha1.........." ); return "hello" ; } @RequestMapping ( "v2/hello/" ) @ResponseBody public String hello2(HttpServletRequest request){ System.out.println( "haha2........." ); return "hello" ; } } |
如今咱們就能夠經過 /v1/hello, /v2/hello 來分別訪問v1和v2兩個版本對應的接口了。這看起來好像能夠解決問題,由於咱們每次某個接口有變更,只要新寫一個對應該版本的方法就能夠了。可是相應的問題也就來了:
- 咱們通常發佈出去的接口,都是以http://api.custom.com/v1,http://api.custom.com/v2發佈出去的,從v1到v2,每每咱們只會變更其中一小部分接口,可是客戶端必需統一版本號調用 。
- 不能智能向上兼容接口。若是如今咱們某個接口最高版本是v2,如 /v2/hello, 如今經過 /v3/hello 要可以自動適配到 /v2/hello上。
因此咱們經過Spring強大的擴展機制增長几個擴展類來完成這個工做。先看下SringMVC中HandlerMapping加載初始化和動態根據url到handler的流程:

能夠看到,HandlerMapping就是經過繼承InitializingBean接口在完成實例後,掃描全部的Controller和標識RequestMapping的方法,緩存這個映射對應關係。而後在應用運行的時候,根據請求的request來找到相應的handler來處理這個請求。因此,咱們添加擴展類:
- ApiVersion
- ApiVesrsionCondition
- CustomRequestMappingHandlerMapping
- WebConfig
現分別來看下這個類,首先看下ApiVersion這個註解:
1 2 3 4 5 6 7 8 9 10 11 |
@Target ({ElementType.METHOD, ElementType.TYPE}) @Retention (RetentionPolicy.RUNTIME) @Documented @Mapping public @interface ApiVersion { /** * 版本號 * @return */ int value(); } |
這個註解用來標識某個類或者方法要處理的對應版本號,使用以下:
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 |
@Controller @RequestMapping ( "/{version}/" ) public class HelloController { @RequestMapping ( "hello/" ) @ApiVersion ( 1 ) @ResponseBody public String hello(HttpServletRequest request){ System.out.println( "haha1.........." ); return "hello" ; } @RequestMapping ( "hello/" ) @ApiVersion ( 2 ) @ResponseBody public String hello2(HttpServletRequest request){ System.out.println( "haha2........." ); return "hello" ; } @RequestMapping ( "hello/" ) @ApiVersion ( 5 ) @ResponseBody public String hello5(HttpServletRequest request){ System.out.println( "haha5........." ); return "hello" ; } } |
如今咱們就能夠經過 /v1/hello/, /v2/hello/, /v5/hello來分別調用版本1,2,5的管理。固然咱們也要解決剛纔說的兩點問題,若是用戶經過 /v4/hello/來訪問接口,則要自動適配到 /v2/hello/,由於 v2是比v4低的版本中最新的版本。
再來看下 ApiVersionCondition 這個類。這個類就是咱們自定義一個條件篩選器,讓SpringMVC在原有邏輯的基本上添加一個版本號匹配的規則:
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 |
public class ApiVesrsionCondition implements RequestCondition<ApiVesrsionCondition> { // 路徑中版本的前綴, 這裏用 /v[1-9]/的形式 private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile( "v(\\d+)/" ); private int apiVersion; public ApiVesrsionCondition( int apiVersion){ this .apiVersion = apiVersion; } public ApiVesrsionCondition combine(ApiVesrsionCondition other) { // 採用最後定義優先原則,則方法上的定義覆蓋類上面的定義 return new ApiVesrsionCondition(other.getApiVersion()); } public ApiVesrsionCondition getMatchingCondition(HttpServletRequest request) { Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getPathInfo()); if (m.find()){ Integer version = Integer.valueOf(m.group( 1 )); if (version >= this .apiVersion) // 若是請求的版本號大於配置版本號, 則知足 return this ; } return null ; } public int compareTo(ApiVesrsionCondition other, HttpServletRequest request) { // 優先匹配最新的版本號 return other.getApiVersion() - this .apiVersion; } public int getApiVersion() { return apiVersion; } } |
要把這個篩選規則生效的話,要擴展原胡的HandlerMapping,把這個規則設置進去生效,看下CustomRequestMappingHandlerMapping的代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping { @Override protected RequestCondition<ApiVesrsionCondition> getCustomTypeCondition(Class<?> handlerType) { ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion. class ); return createCondition(apiVersion); } @Override protected RequestCondition<ApiVesrsionCondition> getCustomMethodCondition(Method method) { ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion. class ); return createCondition(apiVersion); } private RequestCondition<ApiVesrsionCondition> createCondition(ApiVersion apiVersion) { return apiVersion == null ? null : new ApiVesrsionCondition(apiVersion.value()); } } |
最後,得讓SpringMVC加載咱們定義的CustomRequestMappingHandlerMapping以覆蓋原先的RequestMappingHandlerMapping, 因此要去掉前面說的<mvc:annotation-driven/>這個配置,咱們經過JavaConfig的方式注入:
1 2 3 4 5 6 7 8 9 10 11 12 |
@Configuration public class WebConfig extends WebMvcConfigurationSupport{ @Override @Bean public RequestMappingHandlerMapping requestMappingHandlerMapping() { RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping(); handlerMapping.setOrder( 0 ); handlerMapping.setInterceptors(getInterceptors()); return handlerMapping; } } |
Over!
詳細代碼: https://github.com/hongfuli/study_notes/tree/master/spring/samples
參考:
http://stackoverflow.com/questions/10312177/how-to-implement-requestmapping-custom-properties/10336769#10336769
https://jira.spring.io/browse/SPR-9344