首頁 > 軟體

springboot 實現介面灰度釋出的範例詳解

2022-03-15 16:03:01

前言

對灰度釋出有所瞭解的同學應該知道,灰度釋出的目的之一,就是能夠根據業務規則的調整,互動上呈現不同的形式,舉例來說,當前有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 灰度釋出內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com