前面介绍了Spring Boot 如何快速实现Restful api 接口,并以人员信息为例,设计了一套操作人员信息的接口。不清楚的可以看之前的文章:https://www.cnblogs.com/zhangweizhong/category/1657780.html

有些人可能会问,为什么我看到很多公司的api接口文档里面,都有/api/v1/ 这样的地址呢?其实,/api 就是为了和一般的业务地址区分,标明这个地址是api 的接口。v1 则代表版本号。

可能很多人又会问了,为什么要版本号呢?那么,接下来就聊一聊Restful 接口为什么要加版本号? 如何优雅的设计 Restful API 接口版本号?

一、为什么加版本号

一般来说,api 接口是提供给其他系统或是其他公司使用,不能随意频繁的变更。然而,需求和业务不断变化,接口和参数也会发生相应的变化。如果直接对原来的接口进行修改,势必会影响线其他系统的正常运行。这就必须对api 接口进行有效的版本控制。

例如,添加用户的接口,由于业务需求变化,接口的字段属性也发生了变化而且可能和之前的功能不兼容。为了保证原有的接口调用方不受影响,只能重新定义一个新的接口。

Api 版本控制的方式:

  1、域名区分管理,即不同的版本使用不同的域名,v1.api.test.com,v2.api.test.com

  2、请求url 路径区分,在同一个域名下使用不同的url路径,test.com/api/v1/,test.com/api/v2

  3、请求参数区分,在同一url路径下,增加version=v1或v2 等,然后根据不同的版本,选择执行不同的方法。

实际项目中,一般选择第二种:请求url路径区分。因为第二种既能保证水平扩展,有不影响以前的老版本。

二、Spring Boot如何实现

实现方案:

1、首先创建自定义的@APIVersion 注解和自定义URL匹配规则ApiVersionCondition。

2、然后创建自定义的 RequestMappingHandlerMapping 匹配对应的request,选择符合条件的method handler。

1、创建自定义注解

首先,在com.weiz.config 包下,创建一个自定义版本号标记注解 @ApiVersion。

  1. package com.weiz.config;
  2.  
  3. import java.lang.annotation.ElementType;
  4. import java.lang.annotation.Retention;
  5. import java.lang.annotation.RetentionPolicy;
  6. import java.lang.annotation.Target;
  7.  
  8. /**
  9. * API版本控制注解
  10. */
  11. @Target({ElementType.TYPE})
  12. @Retention(RetentionPolicy.RUNTIME)
  13. public @interface ApiVersion {
  14. /**
  15. * @return 版本号
  16. */
  17. int value() default 1;
  18. }

说明:

  1.  ApiVersion 为自定义的注解,API版本控制,返回对应的版本号。

2、自定义url匹配逻辑

创建 ApiVersionCondition 类,并继承RequestCondition 接口,作用是:版本号筛选,将提取请求URL中版本号,与注解上定义的版本号进行比对,以此来判断某个请求应落在哪个controller上。

在com.weiz.config 包下创建ApiVersionCondition 类,重写 RequestCondition,创建自定义的url匹配逻辑。

  1. package com.weiz.config;
  2.  
  3. import org.springframework.web.servlet.mvc.condition.RequestCondition;
  4.  
  5. import javax.servlet.http.HttpServletRequest;
  6. import java.util.regex.Matcher;
  7. import java.util.regex.Pattern;
  8.  
  9. public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
  10. private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile(".*v(\\d+).*");
  11.  
  12. private int apiVersion;
  13.  
  14. ApiVersionCondition(int apiVersion) {
  15. this.apiVersion = apiVersion;
  16. }
  17.  
  18. private int getApiVersion() {
  19. return apiVersion;
  20. }
  21.  
  22. @Override
  23. public ApiVersionCondition combine(ApiVersionCondition apiVersionCondition) {
  24. return new ApiVersionCondition(apiVersionCondition.getApiVersion());
  25. }
  26.  
  27. @Override
  28. public ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {
  29. Matcher m = VERSION_PREFIX_PATTERN.matcher(httpServletRequest.getRequestURI());
  30. if (m.find()) {
  31. Integer version = Integer.valueOf(m.group(1));
  32. if (version >= this.apiVersion) {
  33. return this;
  34. }
  35. }
  36. return null;
  37. }
  38.  
  39. @Override
  40. public int compareTo(ApiVersionCondition apiVersionCondition, HttpServletRequest httpServletRequest) {
  41. return apiVersionCondition.getApiVersion() - this.apiVersion;
  42. }
  43. }

当方法级别和类级别都有ApiVersion注解时,二者将进行合并(ApiVersionRequestCondition.combine)。最终将提取请求URL中版本号,与注解上定义的版本号进行比对,判断url是否符合版本要求。

3、自定义匹配的处理器

在com.weiz.config 包下创建 ApiRequestMappingHandlerMapping 类,重写部分 RequestMappingHandlerMapping 的方法。

  1. package com.weiz.config;
  2.  
  3. import org.springframework.web.bind.annotation.RequestMapping;
  4. import org.springframework.web.servlet.mvc.condition.RequestCondition;
  5. import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
  6.  
  7. import java.lang.reflect.Method;
  8.  
  9. public class ApiRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
  10. private static final String VERSION_FLAG = "{version}";
  11.  
  12. private static RequestCondition<ApiVersionCondition> createCondition(Class<?> clazz) {
  13. RequestMapping classRequestMapping = clazz.getAnnotation(RequestMapping.class);
  14. if (classRequestMapping == null) {
  15. return null;
  16. }
  17. StringBuilder mappingUrlBuilder = new StringBuilder();
  18. if (classRequestMapping.value().length > 0) {
  19. mappingUrlBuilder.append(classRequestMapping.value()[0]);
  20. }
  21. String mappingUrl = mappingUrlBuilder.toString();
  22. if (!mappingUrl.contains(VERSION_FLAG)) {
  23. return null;
  24. }
  25. ApiVersion apiVersion = clazz.getAnnotation(ApiVersion.class);
  26. return apiVersion == null ? new ApiVersionCondition(1) : new ApiVersionCondition(apiVersion.value());
  27. }
  28.  
  29. @Override
  30. protected RequestCondition<?> getCustomMethodCondition(Method method) {
  31. return createCondition(method.getClass());
  32. }
  33.  
  34. @Override
  35. protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
  36. return createCondition(handlerType);
  37. }
  38. }

4、配置注册自定义的RequestMappingHandlerMapping

重写请求过处理的方法,将之前创建的 ApiRequestMappingHandlerMapping 注册到系统中。

  1. package com.weiz.config;
  2.  
  3. import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
  4. import org.springframework.context.annotation.Configuration;
  5. import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
  6.  
  7. @Configuration
  8. public class WebMvcRegistrationsConfig implements WebMvcRegistrations {
  9. @Override
  10. public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
  11. return new ApiRequestMappingHandlerMapping();
  12. }
  13. }

上面四步,把api 版本控制配置完了。代码看着复杂,其实都是重写spring boot 内部的处理流程。

测试

配置完成之后,接下来编写测试的控制器进行测试。

1、在Controller/api 目录下,分别创建UserV1Controller 和 UserV2Controller

UserV1Controller

  1. @RequestMapping("api/{version}/user")
  2. @RestController
  3. public class UserV1Controller {
  4.  
  5. @GetMapping("/test")
  6. public String test() {
  7. return "version1";
  8. }
  9. @GetMapping("/extend")
  10. public String extendTest() {
  11. return "user v1 extend";
  12. }
  13. }

UserV2Controller

  1. @RequestMapping("api/{version}/user")
  2. @RestController
  3. @ApiVersion(2)
  4. public class UserV2Controller {
  5. @GetMapping("/test")
  6. public String test() {
  7. return "user v2 test";
  8. }
  9. }

2、启动项目后,输入相关地址,查看版本控制是否生效

测试结果:

正确的接口地址

  

继承的接口地址

说明:

  上图的前两个截图说明,请求正确的版本地址,会自动匹配版本的对应接口。当请求的版本大于当前版本时,默认匹配当前版本。

  第三个截图说明,当请求对应的版本不存在接口时,会匹配之前版本的接口,即请求/v2/user/extend 接口时,由于v2 控制器未实现该接口,所以自动匹配v1 版本中的接口。这就是所谓的版本继承。

最后

以上,就把Spring Boot 如何优雅的设计 Restful API 接口版本号,实现 API 版本控制介绍完了。版本控制和权限验证是rest api 的基础,虽然看着比较复杂,但是理解了,要实现还是比较简单的。

这个系列课程的完整源码,也会提供给大家。大家关注我的微信公众号(架构师精进),回复:springboot源码。获取这个系列课程的完整源码。

Spring Boot入门系列(二十一)如何优雅的设计 Restful API 接口版本号,实现 API 版本控制!的更多相关文章

  1. Spring Boot入门系列(二十)快速打造Restful API 接口

    spring boot入门系列文章已经写到第二十篇,前面我们讲了spring boot的基础入门的内容,也介绍了spring boot 整合mybatis,整合redis.整合Thymeleaf 模板 ...

  2. Spring Boot入门系列(十六)使用pagehelper实现分页功能

    之前讲了Springboot整合Mybatis,然后介绍了如何自动生成pojo实体类.mapper类和对应的mapper.xml 文件,并实现最基本的增删改查功能.接下来要说一说Mybatis 的分页 ...

  3. Spring Boot入门系列(十七)整合Mybatis,创建自定义mapper 实现多表关联查询!

    之前讲了Springboot整合Mybatis,介绍了如何自动生成pojo实体类.mapper类和对应的mapper.xml 文件,并实现最基本的增删改查功能.mybatis 插件自动生成的mappe ...

  4. Spring Boot入门系列(十三)如何实现事务

    前面介绍了Spring Boot 中的整合Mybatis并实现增删改查.不清楚的朋友可以看看之前的文章:https://www.cnblogs.com/zhangweizhong/category/1 ...

  5. spring boot 入门操作(二)

    spring boot入门操作 使用FastJson解析json数据 pom dependencies里添加fastjson依赖 <dependency> <groupId>c ...

  6. Spring boot入门(二):Spring boot集成MySql,Mybatis和PageHelper插件

    上一篇文章,写了如何搭建一个简单的Spring boot项目,本篇是接着上一篇文章写得:Spring boot入门:快速搭建Spring boot项目(一),主要是spring boot集成mybat ...

  7. Spring Boot 入门系列(二十二)使用Swagger2构建 RESTful API文档

    前面介绍了如何Spring Boot 快速打造Restful API 接口,也介绍了如何优雅的实现 Api 版本控制,不清楚的可以看我之前的文章:https://www.cnblogs.com/zha ...

  8. Spring Boot 入门系列(二十三)整合Mybatis,实现多数据源配置!

    d之前介绍了Spring Boot 整合mybatis 使用注解方式配置的方式实现增删改查以及一些复杂自定义的sql 语句 .想必大家对spring boot 项目中,如何使用mybatis 有了一定 ...

  9. Spring Boot 入门系列(二十四)多环境配置,3分钟搞定!

    之前讲过Spring Boot 的系统配置和自定义配置,实现了按照实际项目的要求配置系统的相关熟悉.但是,在实际项目开发过程中,需要面对不同的环境,例如:开发环境,测试环境,生产环境.各个环境的数据库 ...

随机推荐

  1. [LeetCode]739. 每日温度(单调栈)

    题目 根据每日 气温 列表,请重新生成一个列表,对应位置的输入是你需要再等待多久温度才会升高超过该日的天数.如果之后都不会升高,请在该位置用 0 来代替. 例如,给定一个列表 temperatures ...

  2. SpringBoot(20)---断言(Assert)

    SpringBoot(20)---断言(Assert) 我们在写单元测试的时候,除了接口直接抛异常而导致该单元测试失败外,还有种是业务上的错误也代表着该单元测试失败.好比我们在测试接口的时候, 该接口 ...

  3. 踩坑了,JDK8中HashMap依然会死循环!

    是否你听说过JDK8之后HashMap已经解决的扩容死循环的问题,虽然HashMap依然说线程不安全,但是不会造成服务器load飙升的问题. 然而事实并非如此.少年可曾了解一种红黑树成环的场景,=v= ...

  4. Redis Cluster集群架构实现

    Redis集群简介 通过前面三篇博客的介绍<Redis基础认识及常用命令使用(一)–技术流ken>,<Redis基础知识补充及持久化.备份介绍(二)–技术流ken>,<R ...

  5. java 多线程-4

    十四.sleep方法和wait方法的区别 [面试题] 相同点: 一旦执行方法,都可以使得当前线程进入阻塞状态. 不同点: 两个方法的声明位置不同:Thread类声明sleep():Object类中声明 ...

  6. 使用vue-cli(vue脚手架)快速搭建项目

    vue-cli 是一个官方发布 vue.js 项目脚手架,使用 vue-cli 可以快速创建 vue 项目.这篇文章将会从实操的角度,介绍整个搭建的过程. 1. 避坑前言 其实这次使用vue-cli的 ...

  7. Java 中 static 的作用

    static 关键字的作用 在 Java 中 static 关键字有4种使用场景,下面分别进行介绍: 1.static 成员变量 public class Student { // 静态成员变量 pr ...

  8. 搜索引擎学习(一)初识Lucene

    一.Lucene相关基础概念 定义:一个简易的工具包,实现文件搜索的功能,支持中文,关键字,多条件查询,凡是文件名或文件内容包含的都查出来. 数据分类:结构化数据(固定格式或有限长度的数据)和非结构化 ...

  9. 浅谈Vue中计算属性computed的实现原理

    虽然目前的技术栈已由Vue转到了React,但从之前使用Vue开发的多个项目实际经历来看还是非常愉悦的,Vue文档清晰规范,api设计简洁高效,对前端开发人员友好,上手快,甚至个人认为在很多场景使用V ...

  10. springmvc 源码分析(三) -- 自定义处理器映射器和自定义处理器适配器,以及自定义参数解析器 和错误跳转自定页面

    测试环境搭建: 本次搭建是基于springboot来实现的,代码在码云的链接:https://gitee.com/yangxioahui/thymeleaf.git DispatcherServlet ...