rest 讓SpringMVC支持可版本管理的Restful接口



需求

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

SpringMVC原理概述

SpringMVC核心思想就是通過一個servlet(DispatchServlet)把請求轉發到各個執行方法上(Controller的method),截張官方的圖如下:

 就是把某個形式的URL(當然,url不是唯一的決定條件,還有比如請求方法,get還是post,請求頭中的信息)映射到某個類的具體方法上,這個核心的組件在SpringMVC中叫做: HandlerMapping。我們一般在spring的config文件中做如下配置時會自動初始化加載一個HanlderMapping的實現類:RequestMappingHandlerMapping:

<mvc:annotation-driven/>

至於這個一行的配置幹了什麼,可以從org.springframework.web.servlet.config.MvcNamespaceHandler這個類開始看進去。我們現在來定義一個Controller,如下:

@Controller
public class HelloController {
    @RequestMapping("hello/")
    @ResponseBody
    public String hello(HttpServletRequest request){
        System.out.println("haha1..........");
        return "hello";
    }
}

這樣我們通過 /hello/ 就可以調用了。現在假如我們針對這個接口的業務出現了很大的變化(涉及到字段,報文的改變,和之前的不能兼容),但是老的接口又不能廢棄,因爲你不能保證放出去的接口沒有人調用。所以我們只能把代碼改成如下支持多個版本接口:

@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這個註解:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
    /**
     * 版本號
     * @return
     */
    int value();
}

這個註解用來標識某個類或者方法要處理的對應版本號,使用如下:

@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在原有邏輯的基本上添加一個版本號匹配的規則:

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的代碼:

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的方式注入:

@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

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章