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

前面介绍了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。

package com.weiz.config;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * API版本控制注解
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
    /**
     * @return 版本号
     */
    int value() default 1;
}

说明:

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

2、自定义url匹配逻辑

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

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

package com.weiz.config;

import org.springframework.web.servlet.mvc.condition.RequestCondition;

import javax.servlet.http.HttpServletRequest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ApiVersionCondition implements RequestCondition {
    private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile(".*v(\\d+).*");

    private int apiVersion;

    ApiVersionCondition(int apiVersion) {
        this.apiVersion = apiVersion;
    }

    private int getApiVersion() {
        return apiVersion;
    }

    @Override
    public ApiVersionCondition combine(ApiVersionCondition apiVersionCondition) {
        return new ApiVersionCondition(apiVersionCondition.getApiVersion());
    }

    @Override
    public ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {
        Matcher m = VERSION_PREFIX_PATTERN.matcher(httpServletRequest.getRequestURI());
        if (m.find()) {
            Integer version = Integer.valueOf(m.group(1));
            if (version >= this.apiVersion) {
                return this;
            }
        }
        return null;
    }

    @Override
    public int compareTo(ApiVersionCondition apiVersionCondition, HttpServletRequest httpServletRequest) {
        return apiVersionCondition.getApiVersion() - this.apiVersion;
    }
}

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

3、自定义匹配的处理器

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

package com.weiz.config;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.lang.reflect.Method;

public class ApiRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
    private static final String VERSION_FLAG = "{version}";

    private static RequestCondition createCondition(Class clazz) {
        RequestMapping classRequestMapping = clazz.getAnnotation(RequestMapping.class);
        if (classRequestMapping == null) {
            return null;
        }
        StringBuilder mappingUrlBuilder = new StringBuilder();
        if (classRequestMapping.value().length > 0) {
            mappingUrlBuilder.append(classRequestMapping.value()[0]);
        }
        String mappingUrl = mappingUrlBuilder.toString();
        if (!mappingUrl.contains(VERSION_FLAG)) {
            return null;
        }
        ApiVersion apiVersion = clazz.getAnnotation(ApiVersion.class);
        return apiVersion == null ? new ApiVersionCondition(1) : new ApiVersionCondition(apiVersion.value());
    }

    @Override
    protected RequestCondition getCustomMethodCondition(Method method) {
        return createCondition(method.getClass());
    }

    @Override
    protected RequestCondition getCustomTypeCondition(Class handlerType) {
        return createCondition(handlerType);
    }
}

4、配置注册自定义的RequestMappingHandlerMapping

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

package com.weiz.config;

import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

@Configuration
public class WebMvcRegistrationsConfig implements WebMvcRegistrations {
    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new ApiRequestMappingHandlerMapping();
    }
}

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

测试

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

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

UserV1Controller

@RequestMapping("api/{version}/user")
@RestController
public class UserV1Controller {

    @GetMapping("/test")
    public String test() {
        return "version1";
    }
    @GetMapping("/extend")
    public String extendTest() {
        return "user v1 extend";
    }
}

UserV2Controller

@RequestMapping("api/{version}/user")
@RestController
@ApiVersion(2)
public class UserV2Controller {
    @GetMapping("/test")
    public String test() {
        return "user v2 test";
    }
}

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

测试结果:

正确的接口地址

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

继承的接口地址

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

说明:

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

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

最后

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

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

Original: https://www.cnblogs.com/zhangweizhong/p/13170061.html
Author: 章为忠
Title: Spring Boot入门系列(二十一)如何优雅的设计 Restful API 接口版本号,实现 API 版本控制!

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/540535/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • java 百度人脸识别接口调用配置

    package org.fh.util; <span class="hljs-keyword">import org.json.JSONObject…

    Java 2023年6月7日
    060
  • Java项目实战——瑞吉外卖Day01

    笔记内容为 黑马程序员视频内容 视频地址: https://www.bilibili.com/video/BV13a411q753?p=3&spm_id_from=page…

    Java 2023年5月29日
    078
  • 理解Android Binder机制(3/3):Java层

    在AOSP源码树中的路径 // Binder Framework JNI /frameworks/base/core/jni/android_util_Binder.h /fram…

    Java 2023年5月29日
    099
  • 整数除法

    给定两个整数 a 和 b ,求它们的除法的商 a/b ,要求不得使用乘号 ‘*’、除号 ‘/’ 以及求余符号 ‘%’ 。 注意: 整数除法的结果应当截去( truncate)其小数…

    Java 2023年6月7日
    078
  • 关系型数据库的几种常用主键

    一般来说关系型数据库,绝大多数表都有数据库主键。 数据库主键的创建,一般有如下几种形式: 使用数据库自增长主键的语法。 有些数据库,比如 MS SQL Server, MySQL …

    Java 2023年6月9日
    066
  • Spring RestTemplate中几种常见的请求方式

    在Spring Cloud中服务的发现与消费一文中,当我们从服务消费端去调用服务提供者的服务的时候,使用了一个很好用的对象,叫做RestTemplate,当时我们只使用了RestT…

    Java 2023年5月30日
    085
  • 微服务 mybatis-plus 添加日志输出

    spring配置 spring: redis: database: 6 host: 192.168.8.248 port: 6379 password: datasource: d…

    Java 2023年5月30日
    079
  • ActiveMQ、RabbitMQ、RocketMQ、Kafka四种消息中间件分析介绍

    ActiveMQ、RabbitMQ、RocketMQ、Kafka四种消息中间件分析介绍 我们从四种消息中间件的介绍到基本使用,以及高可用,消息重复性,消息丢失,消息顺序性能方面进行…

    Java 2023年6月8日
    0106
  • 老徐和阿珍的故事:ArrayList和LinkedList的效率到底哪个高?

    人物背景:老徐,男,本名徐福贵,从事Java相关研发工作多年,职场老油条,摸鱼小能手,虽然岁数不大但长的比较着急,人称老徐。据说之前炒某币败光了所有家产,甚至现在还有欠债。阿珍,女…

    Java 2023年6月7日
    070
  • @Autowired注解 –required a single bean, but 2 were found出现的原因以及解决方法

    @Autowired注解是spring用来支持依赖注入的核心利器之一,但是我们或多或少都会遇到required a single bean, but 2 were found(2可…

    Java 2023年6月9日
    0119
  • Mac启动SpringBoot InetAddress.getLocalHost() 执行很慢

    scutil –set HostName “localhost” 参考: https://blog.csdn.net/fngang/articl…

    Java 2023年5月30日
    075
  • 分布式中灰度方案实践

    让请求在导航的服务节上点执行; 一、背景简介 分布式系统中会存在这样的开发场景,不同需求可能涉及到对同一个服务的开发,那么该服务在研发期间就会存在多个版本并行的状态,为了保持不同版…

    Java 2023年6月15日
    071
  • Redis压缩列表

    此篇文章是主要介绍Redis在数据存储方面的其中一种方式,压缩列表。本文会介绍1. 压缩列表(ziplist)的使用场景 2.如何达到节约内存的效果?3.压缩列表的存储格式 4. …

    Java 2023年6月14日
    073
  • SpringBoot中配置Logback日志输出

    因为在SpringBoot中默认使用的Logback日志系统,所以SpringBoot已经集成了相关依赖,无需多余的依赖,只需在src/main/resources文件夹下,增加l…

    Java 2023年5月30日
    071
  • 线程池技术记录

    线程池技术记录 使用线程池的好处 降低资源消耗;降低频繁的创建、销毁线程带来的额外开销,复用已创建的现场。 降低使用的复杂度;将任务的提交和执行进行解耦,我们只需要创建一个线程池,…

    Java 2023年6月7日
    086
  • 《Head First设计模式》读书笔记

    前言:本文是记录我在阅读《Head First设计模式》这本书时,做得相关笔记,相关示例代码地址:design-patterns。由于本书不是将设计原则和设计模式分开讲述的,而是在…

    Java 2023年6月5日
    093
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球