springboot 实现接口灰度发布的实例详解

目录
  • 前言
  • 最小化改造方式
  • springmvc接口请求原理
    • HandlerMapping简介
    • RequestCondition接口定义
  • 代码实现过程
    • 1、添加一个自定义注解用于标注接口类以及接口方法
    • 2、自定义HandleMapping
    • 3、自定义封装RequestCondition
    • 4、注册自定义的ApiVersionHandleMapping
    • 5、接口测试

前言

对灰度发布有所了解的同学应该知道,灰度发布的目的之一,就是能够根据业务规则的调整,交互上呈现不同的形式,举例来说,当前有2个版本,V1.0和V2.0 ,那么可能表现的形式大概有下面几种:

  • V1.0,界面上的交互形态为A,V2.0版本界面上的交互形式为B;
  • 某个交互,针对同一个接口A来说,V1.0,请求接口A,要求的返回值包括5个字段;V2.0,请求接口A,要求返回值包括10个字段;
  • 某个交互,在V1.0和V2.0中,将使用不同的接口;

实际情况可能会更复杂,在微服务广泛使用的今天,一般的思路是,通过一个获取配置的接口,前端拿到所有的参数配置,根据参数配置的不同,具体实现思路如下:

  • 比如V1版本下,某个配置的值为1,这时候使用A交互;如果要使用交互B,只需要更改配置中心这个值为2,则前端就可以将交互切位B;
  • 或者说,交互不变,但是交互的处理逻辑更复杂了,于是原来的接口无法再满足要求,这时候,可以重新提供一个接口,同样通过配置参数的不同来控制;

于是,从后端接口层面来说,一个比较常用也是通用的处理方式是,通过配置接口来达到切换交互,或者说达到灰度发布的目的,灰度发布的核心本质也正在于通过某种方式从一种数据形态切换到另一种形态;

最小化改造方式

上面聊到了通过配置参数接口来达到灰度的目的,事实上,在一些规模较小的项目中,并没有接入分布式配置中心的情况下,可能上面的解决办法并不是一个很好的方式;

举例来说,灰度要达到的目的是,V1.0 的 获取用户列表的接口返回的是本月新增的用户,而V2.0要求返回最近2个月注册的用户,而且接口地址不变,最多就是在参数上面允许适当变更,即做到前端最小化改动;

这个需求,乍然一想,觉得很是不可思议,一个controller类里面,两个同样的接口映射路径肯定不行的啊,比如看下面这个例子,

@RestController
public class UserController {
    @Autowired
    private UserService userService;
    @GetMapping("/list")
    public Object getUserLists1(){
        return userService.getUserLists1();
    }

    @GetMapping("/list")
    public Object getUserLists2(){
        return userService.getUserLists2();
    }

}

当前请求接口时,直接报错了,这个错误想必大家都能理解吧,我就不过多做解释了

springmvc接口请求原理

下面贴出一张关于springmvc接口请求原理的流程图,即一个请求最终到达某个具体的controller时经历的一个完整的过程,相信有个SSM开发或者springboot开发经验的同学对这个图应该不陌生;

从大的分类上,主要包括下面几个核心处理组件:

  • Dispatcher Servlet ,请求分发器,收到请求调用处理器映射器HandlerMapping;
  • HandlerMapping,HandlerAdapter,处理器映射器和处理器适配器,根据请求的url地址,定位到具体的controller中的具体的处理方法;
  • View Resolver,视图解析器 ,解析接口的返回数据并返回具体View给Dispatcher Servlet ;

在上面这几个组件中,需要重点关注这个叫做 HandlerMapping 的组件,为了实现上文谈到的灰度发布功能,就需要好好研究下HandlerMapping的原理;

HandlerMapping简介

HandlerMapping在这个SpringMVC体系结构中有着举足轻重的地位,充当着url和Controller之间映射关系配置的角色,主要有三部分组成:

  • HandlerMapping 映射注册;
  • 根据url获取对应的处理器;
  • 拦截器注册

在springmvc中,其核心类为 RequestMappingHandlerMapping ,该类中的囊括了与请求映射处理相关的所有实现,举例来说,

  • match(HttpServletRequest request, String pattern) ,通过里面的match方法,可以将request中的请求路径与规则路径做匹配;
  • registerHandlerMethod,注册处理器;

在该类中,我们注意到这样两个如下的方法,但是其方法内部无任何的实现逻辑,对spring源码稍有了解的同学应该知道,这个肯定是spring框架对于该类预留出来的可供开发中扩展的方法,而这两个方法就是用于实现本次需求的两个核心方法;

我们注意到两个方法的返回值均为RequestCondition,即请求条件的对象,从上面了解到HandlerMapping 是在容器初始化执行,那么一定有一个时机,只要客户端重写了HandlerMapping的这两个方法内部的逻辑,就可以通过解析handleType的参数,达到通过某种参数条件,满足本文的最小化前端改造的需求;

关于RequestCondition几点补充:

  • RequestCondition是Spring MVC对一个请求匹配条件的概念建模;
  • 实现类可能是针对以下情况之一:路径匹配,头部匹配,请求参数匹配,可产生MIME匹配,可消费MIME匹配,请求方法匹配,或者是以上各种情况的匹配条件的一个组合;

RequestCondition接口定义

public interface RequestCondition<T> {
    //和另外一个请求匹配条件合并,具体合并逻辑由实现类提供
    T combine(T var1);

    // 检查当前请求匹配条件和指定请求request是否匹配,如果不匹配返回null,
    // 如果匹配,生成一个新的请求匹配条件,该新的请求匹配条件是当前请求匹配条件
    // 针对指定请求request的剪裁。
    // 举个例子来讲,如果当前请求匹配条件是一个路径匹配条件,包含多个路径匹配模板,
    // 并且其中有些模板和指定请求request匹配,那么返回的新建的请求匹配条件将仅仅
    // 包含和指定请求request匹配的那些路径模板。
    @Nullable
    T getMatchingCondition(HttpServletRequest var1);

    // 针对指定的请求对象request比较两个请求匹配条件。
    // 该方法假定被比较的两个请求匹配条件都是针对该请求对象request调用了
    // #getMatchingCondition方法得到的,这样才能确保对它们的比较
    // 是针对同一个请求对象request,这样的比较才有意义(最终用来确定谁是
    // 更匹配的条件)。
    int compareTo(T var1, HttpServletRequest var2);
}

由接口源代码可以看出,接口RequestCondition是一个泛型接口。事实上,它的泛型参数T通常也会是一个RequestCondition对象,搞清这一点就能和上面的HandlerMapping中的两个即将要重写的方法就能产生联系了;

代码实现过程

1、添加一个自定义注解用于标注接口类以及接口方法

通过上面的分析,我们了解到可以通过HandlerMapping 中的getCustomTypeCondition方法和getCustomMethodCondition方法,读取到接口类或者接口方法中的元信息,比如接口路径,注解,方法名称等,

怎样才能实现前端的最小化改造呢?主要思路是,通过参数控制的形式,比如前端不用改动原来的接口地址,只需传入不同的参数即可满足要求,于是可以通过自定义注解的形式,给不同的方法添加注解,通过封装注解参数为RequestCondition的方式来实现;

import java.lang.annotation.*;

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

    //具体版本号
    double value();

}

2、自定义HandleMapping

新增一个类,继承RequestMappingHandlerMapping,重写里面的两个方法,封装成RequestCondition提供后续调用;

import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.lang.reflect.Method;

/**
 * 支持使用多版本的控制器
 */
public class ApiVersionHandleMapping extends RequestMappingHandlerMapping {

    /**
     * 容器初始化执行
     * 所有controller都会使用该方法
     * @param handlerType
     * @return
     */
    @Override
    protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
        ApiVersion apiVersion = AnnotationUtils.getAnnotation(handlerType, ApiVersion.class);
        return new ApiVersionRequestCondition(apiVersion != null ? apiVersion.value() : 1.0);
    }

    /**
     * 容器初始化时执行
     * @param method
     * @return
     */
    @Override
    protected RequestCondition<?> getCustomMethodCondition(Method method) {
        ApiVersion apiVersion = AnnotationUtils.getAnnotation(method, ApiVersion.class);
        if(apiVersion == null){
            apiVersion = AnnotationUtils.getAnnotation(method.getDeclaringClass(), ApiVersion.class);
        }
        return new ApiVersionRequestCondition(apiVersion != null ? apiVersion.value() : 1.0);
    }
}

3、自定义封装RequestCondition

封装子自定义的RequestCondition逻辑,该类会在客户端请求接口时,根据入参进行一系列的与真正的执行接口进行匹配的逻辑操作,比如,默认情况下,如果请求URL中不传入任何参数,将返回默认的 V1.0的接口;

import org.apache.commons.lang.StringUtils;
import org.springframework.web.servlet.mvc.condition.RequestCondition;

import javax.servlet.http.HttpServletRequest;

public class ApiVersionRequestCondition implements RequestCondition<ApiVersionRequestCondition> {

    private double apiVersion = 1.0;

    private static final String VERSION_NAME = "api-version";

    public double getApiVersion() {
        return apiVersion;
    }

    public ApiVersionRequestCondition(double apiVersion){
        this.apiVersion=apiVersion;
    }

    @Override
    public ApiVersionRequestCondition combine(ApiVersionRequestCondition method) {
        return method;
    }

    @Override
    public int compareTo(ApiVersionRequestCondition other, HttpServletRequest request) {
        return Double.compare(other.getApiVersion(),this.getApiVersion());
    }

    @Override
    public ApiVersionRequestCondition getMatchingCondition(HttpServletRequest request) {

        double reqVersionDouble = 1.0;

        String reqVersion = request.getHeader(VERSION_NAME);
        if(StringUtils.isEmpty(reqVersion)){
            reqVersion = request.getParameter(VERSION_NAME);
        }

        if(!StringUtils.isEmpty(reqVersion)){
            reqVersionDouble = Double.parseDouble(reqVersion);
        }

        if(this.getApiVersion() == reqVersionDouble){
            return this;
        }
        return null;
    }
}

4、注册自定义的 ApiVersionHandleMapping

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

public class ApiVersionMappingRegister implements WebMvcRegistrations {

    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new ApiVersionHandleMapping();
    }
}
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BaseConfiguration {

    @Bean
    public WebMvcRegistrations getWebMvcRegistrations(){
        return new ApiVersionMappingRegister();
    }

}

5、接口测试

对本文开篇的接口做简单的改造,添加自定义注解

import com.congge.configs.ApiVersion;
import com.congge.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@ApiVersion(3.0)
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/list")
    @ApiVersion(1.0)
    public Object getUserLists1(){
        return userService.getUserLists1();
    }

    @GetMapping("/list")
    @ApiVersion(2.0)
    public Object getUserLists2(){
        return userService.getUserLists2();
    }
}

启动项目后,做如下接口测试:

1、不添加任何参数,默认不加任何参数,将请求V1版本的接口

2、接口请求中添加 api-version = 2.0 ,将请求到V2对应的接口

通过以上的演示,我们基本上实现了一个基于 springboot 实现接口多版本控制的接口灰度发布的功能。

到此这篇关于springboot 实现接口灰度发布的文章就介绍到这了,更多相关springboot 灰度发布内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 将本地SpringBoot项目发布到云服务器的方法

    如何将本地的SpringBoot项目发布到云服务器 环境.准备 一台云服务器(我的是linux系统) 一个能运行的SpringBoot项目 xsheel或者连接云服务器的软件 编辑器IDEA 首先对本地的项目打包成jar包 1.配置打包项目的依赖 在主pom.xml里添加 <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>

  • SpringBoot事件发布和监听详解

    目录 概述 事件监听的结构 Publisher,Event和Listener的关系 事件 发布者 监听者 总结 概述 ApplicationEvent以及Listener是Spring为我们提供的一个事件监听.订阅的实现,内部实现原理是观察者设计模式,设计初衷也是为了系统业务逻辑之间的解耦,提高可扩展性以及可维护性.事件发布者并不需要考虑谁去监听,监听具体的实现内容是什么,发布者的工作只是为了发布事件而已.事件监听的作用与消息队列有一点类似. 事件监听的结构 主要有三个部分组成: 发布者Publ

  • springboot 实现接口灰度发布的实例详解

    目录 前言 最小化改造方式 springmvc接口请求原理 HandlerMapping简介 RequestCondition接口定义 代码实现过程 1.添加一个自定义注解用于标注接口类以及接口方法 2.自定义HandleMapping 3.自定义封装RequestCondition 4.注册自定义的ApiVersionHandleMapping 5.接口测试 前言 对灰度发布有所了解的同学应该知道,灰度发布的目的之一,就是能够根据业务规则的调整,交互上呈现不同的形式,举例来说,当前有2个版本,

  • springboot config 拦截器使用方法实例详解

    本文介绍Spring-Boot中使用拦截器,一般在拦截器中处理跨域处理,允许跨域访问项目,拦截器使用详细资料请查阅官网. 实现自定义拦截器步骤: 1.创建一个类并实现HandlerInterceptor接口. 2.创建一个Java类继承WebMvcConfigurerAdapter,并重写 addInterceptors 方法. 2.将自定义的拦截器交由spring管理,然后将对像手动添加到拦截器链中(在addInterceptors方法中添加). 创建拦截器类 package com.exam

  • springboot Mongodb的集成与使用实例详解

    说说springboot与大叔lind.ddd的渊源 Mongodb在Lind.DDD中被二次封装过(大叔的.net和.net core),将它当成是一种仓储来使用,对于开发人员来说只公开curd几个标准的接口即可,而在springboot框架里,它与大叔lind有些类似之处,同样是被二次封装了,开发人员只需要关注自己的业务即可,而标准的curd操作完成由springboot帮助我们来实现,一般地,我们会设计一个与实体对象的接口仓储,让它去继承mongo的标准接口,然后在springboot的依

  • SpringBoot 集成Kaptcha实现验证码功能实例详解

    在一个web应用中验证码是一个常见的元素.不管是防止机器人还是爬虫都有一定的作用,我们是自己编写生产验证码的工具类,也可以使用一些比较方便的验证码工具.在网上收集一些资料之后,今天给大家介绍一下kaptcha的和springboot一起使用的简单例子. 准备工作: 1.你要有一个springboot的hello world的工程,并能正常运行. 2.导入kaptcha的maven: <!-- https://mvnrepository.com/artifact/com.github.penggl

  • SpringBoot配置文件的加载位置实例详解

    springboot采纳了建立生产就绪spring应用程序的观点. Spring Boot优先于配置的惯例,旨在让您尽快启动和运行.在一般情况下,我们不需要做太多的配置就能够让spring boot正常运行.在一些特殊的情况下,我们需要做修改一些配置,或者需要有自己的配置属性. SpringBoot启动会扫描以下位置的application.yml或者 application.properties文件作为SpringBoot的默认配置文件. -file:./config/    -file:./

  • Admin - SpringBoot + Maven 多启动环境配置实例详解

    一:父级pom.xml文件 resources目录下新建指定文件夹,存放Spring配置文件 <profiles> <profile> <id>dev</id> <properties> <profiles.active>dev</profiles.active> </properties> <activation> <activeByDefault>true</activeByD

  • SpringBoot项目使用mybatis-plus代码生成的实例详解

    目录 前言 安装依赖 application.yml添加配置 代码生成实例 代码生成依赖 数据源配置 globalConfig处理通用配置 packageConfig包名设置 strategyConfig配置 小结 总结 前言 mybatis-plus官方地址 https://baomidou.com mybatis-plus是mybatis的增强,不对mybatis做任何改变,涵盖了代码生成,自定义ID生成器,快速实现CRUD,自动分页,逻辑删除等功能,更多功能请查阅官方文档 安装依赖 myb

  • SpringBoot + validation 接口参数校验的思路详解

    有参数传递的地方都少不了参数校验.在web开发中,前端的参数校验是为了用户体验,后端的参数校验是为了安全.试想一下,如果在controller层中没有经过任何校验的参数通过service层.dao层一路来到了数据库就可能导致严重的后果,最好的结果是查不出数据,严重一点就是报错,如果这些没有被校验的参数中包含了恶意代码,那就可能导致更严重的后果. 实践 一.引入依赖 <!--引入spring-boot-starter-validation--> <dependency> <gr

  • springboot+mybatis+redis 二级缓存问题实例详解

    前言 什么是mybatis二级缓存? 二级缓存是多个sqlsession共享的,其作用域是mapper的同一个namespace. 即,在不同的sqlsession中,相同的namespace下,相同的sql语句,并且sql模板中参数也相同的,会命中缓存. 第一次执行完毕会将数据库中查询的数据写到缓存,第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率. Mybatis默认没有开启二级缓存,需要在全局配置(mybatis-config.xml)中开启二级缓存. 本文讲述的是使用Redi

  • Java中的接口和抽象类用法实例详解

    本文实例讲述了Java中的接口和抽象类用法.分享给大家供大家参考,具体如下: 在面向对象的概念中,我们知道所有的对象都是通过类来描绘的,但是并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类. 抽象类往往用来表征我们在对问题领域进行分析. 设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象,我们不能把它们实例化(拿不出一个具体的东西)所以称之为抽象. 比如:我们要描述"水果",它就是一个抽象,它有质量.体积等

随机推荐