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

需求

移动互联网时代的到来,软件开发的模式也在变化。记得之前作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

相关文章
相关标签/搜索