一灰灰blog 一灰灰blog
首页
  • InfluxDB
  • MongoDB
  • MySql
  • 基础系列
  • DB系列
  • 搜索系列
  • MQ系列
  • WEB系列
  • 中间件
  • 运维
  • SpringSecurity
  • SpringCloud
  • QuickAlarm
  • QuickCrawer
  • QuickFix
  • QuickMedia
  • QuickSpi
  • QuickTask
  • 高可用
  • 分类
  • 标签
  • 归档
  • 收藏
  • 关于
GitHub (opens new window)

一灰灰blog

资深搬运工
首页
  • InfluxDB
  • MongoDB
  • MySql
  • 基础系列
  • DB系列
  • 搜索系列
  • MQ系列
  • WEB系列
  • 中间件
  • 运维
  • SpringSecurity
  • SpringCloud
  • QuickAlarm
  • QuickCrawer
  • QuickFix
  • QuickMedia
  • QuickSpi
  • QuickTask
  • 高可用
  • 分类
  • 标签
  • 归档
  • 收藏
  • 关于
GitHub (opens new window)
  • 基础系列

  • DB系列

  • 搜索系列

  • MQ系列

  • WEB系列

    • Request

    • Response

    • RestTemplate

    • WebClient

    • WebFlux

    • WebSocket

    • Web三剑客

    • 实例

      • 【WEB系列】Spring MVC之基于xml配置的web应用构建
      • 【WEB系列】Spring MVC之基于java config无xml配置的web应用构建
      • 【WEB系列】一个web demo应用构建全过程
      • 【WEB系列】实现后端的接口版本支持(应用篇)
        • I. 应用场景
          • 1. 约定
          • 2. 规则
        • II. 应用实现
          • 1. 接口定义
          • 2. HandlerMapping接口选择
        • III. 测试
          • case1. 方法上添加版本
          • case2. 类版本+方法版本
        • IV. 其他
          • 0. 项目&相关博文
          • 1. 一灰灰Blog
      • 【WEB系列】徒手撸一个扫码登录示例工程(应用篇)
      • 【WEB系列】一步步实现一个面向接口的网络访问实例(应用篇)
      • 【WEB系列】最小成本实现REST服务扩展(应用篇)
      • 【WEB系列】从0到1实现自定义web参数映射器
    • 其他

  • 中间件

  • 运维

  • SpringSecurity

  • SpringCloud

  • Spring系列
  • WEB系列
  • 实例
一灰灰
2019-12-25

【WEB系列】实现后端的接口版本支持(应用篇)

作为一个主职的后端开发者,在平时的工作中,最讨厌的做的事情可以说是参数校验和接口的版本支持了。对于客户端的同学来说,业务的历史包袱会小很多,当出现不兼容的业务变动时,直接开发新的就好;然而后端就没有这么简单了,历史的接口得支持,新的业务也得支持,吭哧吭哧的新加一个服务接口,url又不能和之前的相同,怎么办?只能在某个地方加一个类似v1, v2...

那么有没有一种不改变url,通过其他的方式来支持版本管理的方式呢?

本文将介绍一种,利用请求头来传递客户端版本,在相同的url中寻找最适合的这个版本请求的接口的实例case

主要用到的知识点为:

  • RequestCondition
  • RequestMappingHandlerMapping

# I. 应用场景

我们希望同一个业务始终用相同的url,即便不同的版本之间业务完全不兼容,通过请求参数中的版本选择最合适的后端接口来响应这个请求

# 1. 约定

需要实现上面的case,首先有两个约定

  • 每个请求中必须携带版本参数
  • 每个接口都定义有一个支持的版本

# 2. 规则

明确上面两点前提之后,就是基本规则了

版本定义

根据常见的三段式版本设计,版本格式定义如下

x.x.x
1
  • 其中第一个x:对应的是大版本,一般来说只有较大的改动升级,才会改变
  • 其中第二个x:表示正常的业务迭代版本号,每发布一个常规的app升级,这个数值+1
  • 最后一个x:主要针对bugfix,比如发布了一个app,结果发生了异常,需要一个紧急修复,需要再发布一个版本,这个时候可以将这个数值+1

接口选择

通常的web请求都是通过url匹配规则来选择对应响应接口,但是在我们这里,一个url,可能会有多个不同的接口,该怎么选择呢?

  • 首先从请求中,获取版本参数 version
  • 从所有相同的url接口中,根据接口上定义的版本,找到所有小于等于version的接口
  • 在上面满足条件的接口中,选择版本最大的接口来响应请求

# II. 应用实现

明确上面的应用场景之后,开始设计与实现

# 1. 接口定义

首先我们需要一个版本定义的注解,用于标记web服务接口的版本,默认版本好为1.0.0

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Api {

    /**
     * 版本
     *
     * @return
     */
    String value() default "1.0.0";
}
1
2
3
4
5
6
7
8
9
10
11
12

其次需要一个版本对应的实体类,注意下面的实现中,默认版本为1.0.0,并实现了Comparable接口,支持版本之间的比较

@Data
public class ApiItem implements Comparable<ApiItem> {

    private int high = 1;

    private int mid = 0;

    private int low = 0;

    public ApiItem() {
    }

    @Override
    public int compareTo(ApiItem right) {
        if (this.getHigh() > right.getHigh()) {
            return 1;
        } else if (this.getHigh() < right.getHigh()) {
            return -1;
        }

        if (this.getMid() > right.getMid()) {
            return 1;
        } else if (this.getMid() < right.getMid()) {
            return -1;
        }

        if (this.getLow() > right.getLow()) {
            return 1;
        } else if (this.getLow() < right.getLow()) {
            return -1;
        }
        return 0;
    }
}
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

需要一个将string格式的版本转换为ApiItem的转换类,并且支持了默认版本为1.0.0的设定

public class ApiConverter {
    public static ApiItem convert(String api) {
        ApiItem apiItem = new ApiItem();
        if (StringUtils.isBlank(api)) {
            return apiItem;
        }

        String[] cells = StringUtils.split(api, ".");
        apiItem.setHigh(Integer.parseInt(cells[0]));
        if (cells.length > 1) {
            apiItem.setMid(Integer.parseInt(cells[1]));
        }

        if (cells.length > 2) {
            apiItem.setLow(Integer.parseInt(cells[2]));
        }
        return apiItem;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 2. HandlerMapping接口选择

需要一个url,支持多个请求接口,可以考虑通过RequestCondition来实现,下面是具体的实现类

public class ApiCondition implements RequestCondition<ApiCondition> {

    private ApiItem version;

    public ApiCondition(ApiItem version) {
        this.version = version;
    }

    @Override
    public ApiCondition combine(ApiCondition other) {
        // 选择版本最大的接口
        return version.compareTo(other.version) >= 0 ? new ApiCondition(version) : new ApiCondition(other.version);
    }

    @Override
    public ApiCondition getMatchingCondition(HttpServletRequest request) {
        String version = request.getHeader("x-api");
        ApiItem item = ApiConverter.convert(version);
        // 获取所有小于等于版本的接口
        if (item.compareTo(this.version) >= 0) {
            return this;
        }

        return null;
    }

    @Override
    public int compareTo(ApiCondition other, HttpServletRequest request) {
        // 获取最大版本对应的接口
        return other.version.compareTo(this.version);
    }
}
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

虽然上面的实现比较简单,但是有必要注意一下两个逻辑

  • getMatchingCondition方法中,控制了只有版本小于等于请求参数中的版本的ApiCondition才满足规则
  • compareTo 指定了当有多个ApiCoondition满足这个请求时,选择最大的版本

自定义RequestMappingHandlerMapping实现类ApiHandlerMapping

public class ApiHandlerMapping extends RequestMappingHandlerMapping {
    @Override
    protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
        return buildFrom(AnnotationUtils.findAnnotation(handlerType, Api.class));
    }

    @Override
    protected RequestCondition<?> getCustomMethodCondition(Method method) {
        return buildFrom(AnnotationUtils.findAnnotation(method, Api.class));
    }

    private ApiCondition buildFrom(Api platform) {
        return platform == null ? new ApiCondition(new ApiItem()) :
                new ApiCondition(ApiConverter.convert(platform.value()));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

注册

@Configuration
public class ApiAutoConfiguration implements WebMvcRegistrations {

    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new ApiHandlerMapping();
    }
}
1
2
3
4
5
6
7
8

基于此,一个实现接口版本管理的微框架已经完成;接下来进入测试环节

# III. 测试

# case1. 方法上添加版本

设计三个接口,一个不加上注解,两外两个添加不同版本的注解

@RestController
@RequestMapping(path = "v1")
public class V1Rest {

    @GetMapping(path = "show")
    public String show1() {
        return "v1/show 1.0.0";
    }

    @Api("1.1.2")
    @GetMapping(path = "show")
    public String show2() {
        return "v1/show 1.1.2";
    }

    @Api("1.1.0")
    @GetMapping(path = "show")
    public String show3() {
        return "v1/show 1.1.0";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在发起请求时,分别不带上版本,带指定版本,来测试对应的响应

  • 从上面的截图可以看出,请求头中没有版本时,默认给一个1.0.0的版本
  • 响应的是小于请求版本的接口中,版本最大的哪一个

# case2. 类版本+方法版本

每个方法上添加版本有点蛋疼,在上面的注解定义中,就支持了类上注解,从实现上也可以看出,当方法和类上都有注解时,选择最大的版本

@Api("2.0.0")
@RestController
@RequestMapping(path = "v2")
public class V2Rest {

    @Api("1.1.0")
    @GetMapping(path = "show")
    public String show0() {
        return "v2/show0 1.1.0";
    }

    @GetMapping(path = "show")
    public String show1() {
        return "v2/show1 2.0.0";
    }

    @Api("2.1.1")
    @GetMapping(path = "show")
    public String show2() {
        return "v2/show2 2.1.1";
    }

    @Api("2.2.0")
    @GetMapping(path = "show")
    public String show3() {
        return "v2/show3 2.2.0";
    }
}
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

根据我们的实现规则,show0和show1都会相应 <2.1.1 的版本请求,这个时候会出现冲突;

  • 从上面的截图中,可以看出来版本小于2.0.0的请求,报的是404错误
  • 请求版本小于2.1.1的请求,报的是冲突异常

# IV. 其他

# 0. 项目&相关博文

  • 工程:https://github.com/liuyueyi/spring-boot-demo (opens new window)
  • 源码: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-case/201-web-api-version (opens new window)

相关博文

  • SpringBoot 系列教程 web 篇之自定义请求匹配条件 RequestCondition (opens new window)

# 1. 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

  • 一灰灰Blog个人博客 https://blog.hhui.top (opens new window)
  • 一灰灰Blog-Spring专题博客 http://spring.hhui.top (opens new window)

一灰灰blog

编辑 (opens new window)
#RequestCondition#应用
上次更新: 2021/10/15, 19:56:22
【WEB系列】一个web demo应用构建全过程
【WEB系列】徒手撸一个扫码登录示例工程(应用篇)

← 【WEB系列】一个web demo应用构建全过程 【WEB系列】徒手撸一个扫码登录示例工程(应用篇)→

最近更新
01
【基础系列】基于maven多环境配置
04-25
02
【WEB系列】内嵌Tomcat配置Accesslog日志文件生成位置源码探索
04-24
03
【搜索系列】ES查询常用实例演示
04-18
更多文章>
Theme by Vdoing | Copyright © 2017-2022 一灰灰Blog
MIT License | 鄂ICP备18017282号 |
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×