SpringMVC支持版本管理的Restful接口

原文鏈接:https://www.cnblogs.com/jcli/p/springmvc_restful_version.html

需求

移動互聯網時代的到來,軟件開發的模式也在變化。記得以前做B/S的後臺開發,基本上沒有Http接口一說,全部是通過渲染模板技術(jsp,freemark)把最終html展示給最終用戶。現在完全變了,基於後臺接口提供方,我們從來不是針對只是瀏覽器展示的後臺輸出,而是各種終端,比如android,ios。所以設計接口的時候一定要小心,一旦放出去的接口可能就永遠都難以變動(除非你強制客戶端用戶升級)。我們知道,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:

1

<mvc:annotation-driven/>

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

1

2

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;

    }

}

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