Spring Cloud Gateway自定義過濾器實戰(zhàn)(觀測斷路器狀態(tài)變化)

歡迎訪問我的GitHub

https://github.com/zq2599/blog_demos

內容:所有原創(chuàng)文章分類匯總及配套源碼,涉及Java、Docker吝羞、Kubernetes秀仲、DevOPS等瞬逊;

本篇概覽

  • 本文是《Spring Cloud Gateway實戰(zhàn)》系列的第七篇,前面的文章咱們學習了各種內置過濾器,還在《Spring Cloud Gateway的斷路器(CircuitBreaker)功能》一文深入研究了斷路器類型的過濾器(理論&實戰(zhàn)&源碼分析皆有),相信聰明的您一定會有此疑問:內置的再多也無法覆蓋全部場景蓝谨,定制才是終極武器

  • 所以今天咱們就來開發(fā)一個自己專屬的過濾器,至于此過濾器的具體功能青团,其實前文已埋下伏筆譬巫,如下圖:

在這里插入圖片描述
  • 簡單來說,就是在一個有斷路器的Spring Cloud Gateway應用中做個自定義過濾器督笆,在處理每個請求時把斷路器的狀態(tài)打印出來芦昔,這樣咱們就能明明白白清清楚楚知道斷路器的狀態(tài)啥時候改變,變成了啥樣娃肿,也算補全了《Spring Cloud Gateway的斷路器(CircuitBreaker)功能》的知識點

  • 過濾器分為全局和局部兩種咕缎,這里咱們選用局部的,原因很簡單:咱們的過濾器是為了觀察斷路器料扰,所以不需要全局生效锨阿,只要在使用斷路器的路由中生效就夠了;

套路提前知曉

  • 咱們先看看自定義局部過濾器的的基本套路:
  1. 新建一個類(我這里名為StatePrinterGatewayFilter.java)记罚,實現(xiàn)GatewayFilter和Ordered接口,重點是filter方法壳嚎,該過濾器的主要功能就在這里面實現(xiàn)
  2. 新建一個類(我這里名為StatePrinterGatewayFilterFactory.java)桐智,實現(xiàn)AbstractGatewayFilterFactory方法末早,其apply方法的返回值就是上一步新建的StatePrinterGatewayFilter的實例,該方法的入參是在路由配置中過濾器節(jié)點下面的配置说庭,這樣就可以根據配置做一些特殊的處理然磷,然后再創(chuàng)建實例作為返回值
  3. StatePrinterGatewayFilterFactory類實現(xiàn)<font color="blue">String name()</font>方法,該方法的返回值就是路由配置文件中過濾器的<font color="red">name</font>
  4. <font color="blue">String name()</font>也可以不實現(xiàn)刊驴,這是因為定義該方法的接口中有默認實現(xiàn)了姿搜,如下圖,這樣您在路由配置文件中過濾器的<font color="red">name</font>只能是<font color="blue">StatePrinter</font>:
在這里插入圖片描述
  1. 在配置文件中捆憎,添加您自定義的過濾器舅柜,該操作和之前的添加內置過濾器一模一樣
  • 以上就是自定義過濾器的基本套路了,可見還是非常簡單的躲惰,接下來的實戰(zhàn)也是按照這個套路來的

  • 在編寫自定義過濾器代碼之前致份,還有個攔路虎等著我們,也就是咱們過濾器的基本功能:如何取得斷路器的狀態(tài)

如何取得斷路器的狀態(tài)

  • 前文的代碼分析中础拨,咱們了解到斷路器的核心功能集中在SpringCloudCircuitBreakerFilterFactory.apply方法中(沒錯氮块,就是剛才提到的apply方法),打開這個類诡宗,如下圖滔蝉,從綠框可見斷路器功能來自名為<font color="blue">cb</font>的對象摄闸,而這個對象是在紅框處由reactiveCircuitBreakerFactory創(chuàng)建的:
在這里插入圖片描述
  • 展開上圖紅框右側的reactiveCircuitBreakerFactory.create方法繼續(xù)看挤渐,最終跟蹤到了ReactiveResilience4JCircuitBreakerFactory類,發(fā)現(xiàn)了一個極其重要的變量阳准,就是下圖紅框中的circuitBreakerRegistry芳悲,它的內部有個ConcurrentHashMap(InMemoryRegistryStore的entryMap)立肘,這里面存放了所有斷路器實例:
在這里插入圖片描述
  • 此時您應該想到了,拿到斷路器的關鍵就是拿到上圖紅框中的<font color ="blue">circuitBreakerRegistry</font>對象名扛,不過怎么拿呢谅年?首先它是私有類型的,其次雖然有個方法返回了該對象肮韧,但是此方法并非public的融蹂,如下圖紅框:
在這里插入圖片描述
  • 這個問題當然難不倒聰明的您了,沒錯弄企,用反射修改此方法的訪問權限超燃,稍后的代碼中咱們就這么干

  • 還剩最后一個問題:circuitBreakerRegistry是ReactiveResilience4JCircuitBreakerFactory的成員變量,這個ReactiveResilience4JCircuitBreakerFactory從哪獲染辛臁意乓?

  • 如果您配置過斷路器,對這個ReactiveResilience4JCircuitBreakerFactory就很熟悉了约素,設置該對像是配置斷路器的基本操作届良,回顧一下前文的代碼:

@Configuration
public class CustomizeCircuitBreakerConfig {

    @Bean
    public ReactiveResilience4JCircuitBreakerFactory defaultCustomizer() {

        CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom() //
                .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.TIME_BASED) // 滑動窗口的類型為時間窗口
                .slidingWindowSize(10) // 時間窗口的大小為60秒
                .minimumNumberOfCalls(5) // 在單位時間窗口內最少需要5次調用才能開始進行統(tǒng)計計算
                .failureRateThreshold(50) // 在單位時間窗口內調用失敗率達到50%后會啟動斷路器
                .enableAutomaticTransitionFromOpenToHalfOpen() // 允許斷路器自動由打開狀態(tài)轉換為半開狀態(tài)
                .permittedNumberOfCallsInHalfOpenState(5) // 在半開狀態(tài)下允許進行正常調用的次數
                .waitDurationInOpenState(Duration.ofSeconds(5)) // 斷路器打開狀態(tài)轉換為半開狀態(tài)需要等待60秒
                .recordExceptions(Throwable.class) // 所有異常都當作失敗來處理
                .build();

        ReactiveResilience4JCircuitBreakerFactory factory = new ReactiveResilience4JCircuitBreakerFactory();
        factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
                .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofMillis(200)).build())
                .circuitBreakerConfig(circuitBreakerConfig).build());

        return factory;
    }
}
  • 既然ReactiveResilience4JCircuitBreakerFactory是spring的bean笆凌,那我們在StatePrinterGatewayFilterFactory類中用Autowired注解就能隨意使用了

  • 至此,理論分析已全部完成士葫,問題都已經解決乞而,開始編碼

源碼下載

名稱 鏈接 備注
項目主頁 https://github.com/zq2599/blog_demos 該項目在GitHub上的主頁
git倉庫地址(https) https://github.com/zq2599/blog_demos.git 該項目源碼的倉庫地址慢显,https協(xié)議
git倉庫地址(ssh) git@github.com:zq2599/blog_demos.git 該項目源碼的倉庫地址爪模,ssh協(xié)議
  • 這個git項目中有多個文件夾,本篇的源碼在<font color="blue">spring-cloud-tutorials</font>文件夾下荚藻,如下圖紅框所示:
在這里插入圖片描述
  • <font color="blue">spring-cloud-tutorials</font>文件夾下有多個子工程屋灌,本篇的代碼是<font color="red">circuitbreaker-gateway</font>,如下圖紅框所示:
在這里插入圖片描述

編碼

  • 前文創(chuàng)建了子工程<font color="blue">circuitbreaker-gateway</font>鞋喇,此工程已添加了斷路器声滥,現(xiàn)在咱們的過濾器代碼就寫在這個工程中是最合適的了

  • 接下來按照套路寫代碼,首先是StatePrinterGatewayFilter.java侦香,代碼中有詳細注釋就不再啰嗦了落塑,要注意的是getOrder方法返回值是10,這表示過濾器的執(zhí)行順序:

package com.bolingcavalry.circuitbreakergateway.filter;

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.vavr.collection.Seq;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.lang.reflect.Method;

public class StatePrinterGatewayFilter implements GatewayFilter, Ordered {

    private ReactiveResilience4JCircuitBreakerFactory reactiveResilience4JCircuitBreakerFactory;

    // 通過構造方法取得reactiveResilience4JCircuitBreakerFactory實例
    public StatePrinterGatewayFilter(ReactiveResilience4JCircuitBreakerFactory reactiveResilience4JCircuitBreakerFactory) {
        this.reactiveResilience4JCircuitBreakerFactory = reactiveResilience4JCircuitBreakerFactory;
    }

    private CircuitBreaker circuitBreaker = null;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 這里沒有考慮并發(fā)的情況罐韩,如果是生產環(huán)境憾赁,請您自行添加上鎖的邏輯
        if (null==circuitBreaker) {
            CircuitBreakerRegistry circuitBreakerRegistry = null;
            try {
                Method method = reactiveResilience4JCircuitBreakerFactory.getClass().getDeclaredMethod("getCircuitBreakerRegistry",(Class[]) null);
                // 用反射將getCircuitBreakerRegistry方法設置為可訪問
                method.setAccessible(true);
                // 用反射執(zhí)行getCircuitBreakerRegistry方法,得到circuitBreakerRegistry
                circuitBreakerRegistry = (CircuitBreakerRegistry)method.invoke(reactiveResilience4JCircuitBreakerFactory);
            } catch (Exception exception) {
                exception.printStackTrace();
            }

            // 得到所有斷路器實例
            Seq<CircuitBreaker> seq = circuitBreakerRegistry.getAllCircuitBreakers();
            // 用名字過濾散吵,myCircuitBreaker來自路由配置中
            circuitBreaker = seq.filter(breaker -> breaker.getName().equals("myCircuitBreaker"))
                    .getOrNull();
        }

        // 取斷路器狀態(tài)龙考,再判空一次,因為上面的操作未必能取到circuitBreaker
        String state = (null==circuitBreaker) ? "unknown" : circuitBreaker.getState().name();

        System.out.println("state : " + state);

        // 繼續(xù)執(zhí)行后面的邏輯
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 10;
    }
}
  • 接下來是StatePrinterGatewayFilterFactory.java矾睦,這里用不上什么配置晦款,所以apply方法的入參也就沒用上,需要注意的是通過Autowired注解拿到了reactiveResilience4JCircuitBreakerFactory枚冗,然后通過構造方法傳遞給了StatePrinterGatewayFilter實例:
package com.bolingcavalry.circuitbreakergateway.filter;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;

@Component
public class StatePrinterGatewayFilterFactory extends AbstractGatewayFilterFactory<Object>
{
    @Autowired
    ReactiveResilience4JCircuitBreakerFactory reactiveResilience4JCircuitBreakerFactory;

    @Override
    public String name() {
        return "CircuitBreakerStatePrinter";
    }

    @Override
    public GatewayFilter apply(Object config)
    {
        return new StatePrinterGatewayFilter(reactiveResilience4JCircuitBreakerFactory);
    }
}
  • 最后是配置文件缓溅,完整的配置文件如下,可見我們將CircuitBreakerStatePrinter過濾器加了進來赁温,放到最后:
server:
  #服務端口
  port: 8081
spring:
  application:
    name: circuitbreaker-gateway
  cloud:
    gateway:
      routes:
        - id: path_route
          uri: http://127.0.0.1:8082
          predicates:
            - Path=/hello/**
          filters:
            - name: CircuitBreaker
              args:
                name: myCircuitBreaker
            - name: CircuitBreakerStatePrinter
  • 再次運行單元測試類CircuitbreakerTest.java坛怪,如下圖紅框所示,斷路器狀態(tài)已經打印出來股囊,至此袜匿,我們可以精確把握斷路器的狀態(tài)變化了:
在這里插入圖片描述

分析請求被filter漏掉的問題

  • 有個很明顯的問題,聰明睿智的您當然不會忽略:上圖綠框中的連續(xù)四個響應稚疹,對應的斷路器狀態(tài)都沒有打印出來居灯,要知道,咱們的過濾器可是要處理每一個請求的,怎么會連續(xù)漏掉四個呢穆壕?

  • 其實原因很容易推理出來:斷路器CircuitBreaker的filter先執(zhí)行待牵,然后才是咱們的CircuitBreakerStatePrinter,而處于開啟狀態(tài)的斷路器會直接返回錯誤給調用方喇勋,其后面的filter都不會執(zhí)行了

  • 那么問題來了:如何控制CircuitBreaker和CircuitBreakerStatePrinter這兩個filter的順序,讓CircuitBreakerStatePrinter先執(zhí)行偎行?

  • CircuitBreakerStatePrinter是咱們自己寫的代碼川背,修改<font color="blue">StatePrinterGatewayFilter.getOrder</font>的返回值可以調整順序,但CircuitBreaker不是咱自己的代碼呀蛤袒,這可如何是好熄云?

  • 老規(guī)矩,看看斷路器的源碼妙真,前文已經分析過了缴允,斷路器最重要的代碼是SpringCloudCircuitBreakerFilterFactory.apply方法,如下圖紅框珍德,生成的filter是GatewayFilter接口的實現(xiàn)類:

在這里插入圖片描述
  • 再看加載過濾器到集合的那段關鍵代碼练般,在RouteDefinitionRouteLocator.loadGatewayFilters方法中,如下圖所示锈候,由于CircuitBreaker的filter并沒有實現(xiàn)Ordered接口薄料,因此執(zhí)行的是紅框中的代碼,代表其順序的值等于<font color="red">i+1</font>泵琳,這個<font color="red">i</font>就是遍歷路由配置中所有過濾器時的一個從零開始的自增變量而已:
在這里插入圖片描述
  • 回顧咱們的路由配置摄职,CircuitBreaker在前,CircuitBreakerStatePrinter在后获列,所以谷市,在添加CircuitBreaker的時候,i等于0击孩,那么CircuitBreaker的order就等于i+1=1了

  • 而CircuitBreakerStatePrinter實現(xiàn)了Ordered接口迫悠,因此不會走紅框中的代碼,其order等于咱們寫在代碼中的值溯壶,咱們寫的是10

  • 所以:CircuitBreaker的order等于1及皂,CircuitBreakerStatePrinter等于10,當然是CircuitBreaker先執(zhí)行了且改!

再次修改

  • 知道了原因验烧,改起來就容易了,我的做法很簡單:StatePrinterGatewayFilter不再實現(xiàn)Ordered又跛,這樣就和CircuitBreaker的filter一樣碍拆,執(zhí)行的是上圖紅框中的代碼,這樣,在配置文件中感混,誰放在前面誰就先執(zhí)行

  • 代碼就不貼出來了端幼,您自行刪除StatePrinterGatewayFilter中和Ordered相關的部分即可

  • 配置文件調整后如下:

server:
  #服務端口
  port: 8081
spring:
  application:
    name: circuitbreaker-gateway
  cloud:
    gateway:
      routes:
        - id: path_route
          uri: http://127.0.0.1:8082
          predicates:
            - Path=/hello/**
          filters:
            - name: CircuitBreakerStatePrinter
            - name: CircuitBreaker
              args:
                name: myCircuitBreaker
  • 改完了,再次運行CircuitbreakerTest.java弧满,如下圖,這一次滑进,每個請求都會打印出此時斷路器的狀態(tài):
在這里插入圖片描述

知識點小結

  • 至此扶关,用于觀測斷路器狀態(tài)的自定義過濾器就算完成了节槐,整個過程還是有不少知識點的铜异,咱們來盤點一下:
  1. 常規(guī)的局部過濾器開發(fā)步驟
  2. 過濾器執(zhí)行順序的邏輯
  3. spring的依賴注入和自動裝配
  4. 斷路器的filter源碼
  5. java的反射基本功

你不孤單董瞻,欣宸原創(chuàng)一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 數據庫+中間件系列
  6. DevOps系列

歡迎關注公眾號:程序員欣宸

微信搜索「程序員欣宸」田巴,我是欣宸壹哺,期待與您一同暢游Java世界...
https://github.com/zq2599/blog_demos

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末管宵,一起剝皮案震驚了整個濱河市攀甚,隨后出現(xiàn)的幾起案子秋度,更是在濱河造成了極大的恐慌荚斯,老刑警劉巖鲸拥,帶你破解...
    沈念sama閱讀 221,331評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捏浊,死亡現(xiàn)場離奇詭異金踪,居然都是意外死亡法希,警方通過查閱死者的電腦和手機靶瘸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,372評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來怨咪,“玉大人,你說我怎么就攤上這事诗眨。” “怎么了匠楚?”我有些...
    開封第一講書人閱讀 167,755評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長芋簿。 經常有香客問我峡懈,道長,這世上最難降的妖魔是什么益咬? 我笑而不...
    開封第一講書人閱讀 59,528評論 1 296
  • 正文 為了忘掉前任帜平,我火速辦了婚禮裆甩,結果婚禮上,老公的妹妹穿的比我還像新娘茉帅。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 68,526評論 6 397
  • 文/花漫 我一把揭開白布樱蛤。 她就那樣靜靜地躺著,像睡著了一般剑鞍。 火紅的嫁衣襯著肌膚如雪昨凡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,166評論 1 308
  • 那天蚁署,我揣著相機與錄音便脊,去河邊找鬼。 笑死光戈,一個胖子當著我的面吹牛哪痰,可吹牛的內容都是我干的。 我是一名探鬼主播田度,決...
    沈念sama閱讀 40,768評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼妒御,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了镇饺?” 一聲冷哼從身側響起乎莉,我...
    開封第一講書人閱讀 39,664評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎奸笤,沒想到半個月后惋啃,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 46,205評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡监右,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,290評論 3 340
  • 正文 我和宋清朗相戀三年边灭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片健盒。...
    茶點故事閱讀 40,435評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡绒瘦,死狀恐怖称簿,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情惰帽,我是刑警寧澤憨降,帶...
    沈念sama閱讀 36,126評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站该酗,受9級特大地震影響授药,放射性物質發(fā)生泄漏。R本人自食惡果不足惜呜魄,卻給世界環(huán)境...
    茶點故事閱讀 41,804評論 3 333
  • 文/蒙蒙 一悔叽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧爵嗅,春花似錦娇澎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,276評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至册招,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間勒极,已是汗流浹背是掰。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留辱匿,地道東北人键痛。 一個月前我還...
    沈念sama閱讀 48,818評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像匾七,于是被迫代替她去往敵國和親絮短。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,442評論 2 359

推薦閱讀更多精彩內容