让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:

?





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;

    }

}

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

时间: 2024-10-26 02:55:41

让SpringMVC支持可版本管理的Restful接口的相关文章

SpringMvc框架 解决在RESTFUL接口后加任意 “.xxx” 绕过权限的问题

问题描述: 框架使用的是SpringMVC.SpringSecurity,在做权限拦截的时候发现一个问题,假设对请求路径/user/detail进行了权限拦截,在访问/user/detail.abc的时候却能有权限访问 问题原因: SpringMVC框架会将“/user/detail.abc”与RequestMapping中的“/user/detail”进行正则匹配,匹配规则为:/user/detail.*,因此请求进来时能将/user/detail.abc交给/user/detail的Cont

Spring Boot 2.x (十):构建优雅的RESTful接口

RESTful 相信在座的各位对于RESTful都是略有耳闻,那么RESTful到底是什么呢? REST(Representational State Transfer)表述性状态转移是一组架构约束条件和原则.满足这些约束条件和原则的应用程序或设计就是RESTful.需要注意的是,REST是设计风格而不是标准.REST通常基于使用HTTP,URI,和XML(标准通用标记语言下的一个子集)以及HTML(标准通用标记语言下的一个应用)这些现有的广泛流行的协议和标准. 也许这段话有些晦涩难懂,换个角度

[转]简单识别 RESTful 接口

     本文描述了识别一个接口是否真的是 RESTful 接口的基本方法.符合 REST 架构风格的接口,称为 RESTful 接口.本文不打算从架构风格的推导方面描述,而是从 HTTP 标准的方面描述.识别的方法同时也是指导实践的原则.       一.是否使用了正确(合适)的方法 目前对于 HTTP 标准滥用较多的,就是方法.谈起 RESTful 接口的方法,很多资料告诉大家,说 GET.POST.PUT.DELETE,分别对应数据库操作的 SELECT.INSERT.UPDATE.DEL

让python bottle框架支持jquery ajax的RESTful风格的PUT和DELETE等请求

这两天在用python的bottle框架开发后台管理系统,接口约定使用RESTful风格请求,前端使用jquery ajax与接口进行交互,使用POST与GET请求时都正常,而Request Method使用PUT或DELETE请求时,直接爆“HTTP Error 405: Method Not Allowed”错误.而ajax提交的Request Method值DELETE也变成了OPTIONS了. 度娘了好多答案,要么说是浏览器不支持,要么说自己重新封装jquery,还有其他的一些方法...

简单识别 RESTful 接口

本文描述了识别一个接口是否真的是 RESTful 接口的基本方法.符合 REST 架构风格的接口,称为 RESTful 接口.本文不打算从架构风格的推导方面描述,而是从 HTTP 标准的方面描述.识别的方法同时也是指导实践的原则. 一.是否使用了正确(合适)的方法 目前对于 HTTP 标准滥用较多的,就是方法.谈起 RESTful 接口的方法,很多资料告诉大家,说 GET.POST.PUT.DELETE,分别对应数据库操作的 SELECT.INSERT.UPDATE.DELETE.其实这种理解是

PHP restful 接口

首先我们来认识下RESTful Restful是一种设计风格而不是标准,比如一个接口原本是这样的: http://www.test.com/user/view/id/1 表示获取id为1的用户信息,如果使用Restful风格,可以变成这样: http://www.test.com/user/1 可以很明显的看出这样做的好处: 1.更简洁的URL,对程序员友好 2.不暴露内部代码结构,更安全 那么,如何实现这个接口呢?首先,我们需要接收到/user/1部分. $path = $_SERVER['P

简淡 RESTful 接口

今天眼睛有点痛,早点下班回来,不想做饭,顿觉无聊,掐指一算,还是写点想法吧.写东西也是一个休息吧.就聊一下互联网的应用程序接口吧. 互联网最流行的应用程序接口,莫过于 RPC 与 RESTful.两者的一个重要区别是如何对待客户端,RPC 把客户端视为整个系统的一部分,服务器与客户端之间紧密耦合.而 RESTful 刚好相反,客户端与服务器之间,仅需要一个入口 URL. 国内绝大多数 Api,包括新浪微博之类的 HTTP/JSON Api,都是 RPC,RPC 的一个常见的问题就是接口的管理问题

实现Restful接口

1.基本介绍 Restful接口的调用,前端一般使用ajax调用,后端可以使用的方法比较多, 本次介绍三种: 1.HttpURLConnection实现 2.HttpClient实现 3.Spring的RestTemplate 2.HttpURLConnection实现 1 @Controller 2 public class RestfulAction { 3 4 @Autowired 5 private UserService userService; 6 7 // 修改 8 @Reques

RESTful 接口实现简明指南

REST 简介 REST 是一个术语的缩写,REpresentational State Transfer,中文直译「表征状态转移」,这是个很拗口的词.我的建议是先不要强行理解,直接看怎么做,等对实施细节有一些了解后,再来看名字会有更深刻的理解.REST 是一套风格约定,RESTful 是它的形容词形式:比如一套实现了 REST 风格的接口,可以称之为 RESTful 接口. REST 对请求的约定 REST 用来规范应用如何在 HTTP 层与 API 提供方进行数据交互:在现阶段,你应该已经很