歡迎訪問我的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)功能》的知識點
過濾器分為全局和局部兩種咕缎,這里咱們選用局部的,原因很簡單:咱們的過濾器是為了觀察斷路器料扰,所以不需要全局生效锨阿,只要在使用斷路器的路由中生效就夠了;
套路提前知曉
- 咱們先看看自定義局部過濾器的的基本套路:
- 新建一個類(我這里名為StatePrinterGatewayFilter.java)记罚,實現(xiàn)GatewayFilter和Ordered接口,重點是filter方法壳嚎,該過濾器的主要功能就在這里面實現(xiàn)
- 新建一個類(我這里名為StatePrinterGatewayFilterFactory.java)桐智,實現(xiàn)AbstractGatewayFilterFactory方法末早,其apply方法的返回值就是上一步新建的StatePrinterGatewayFilter的實例,該方法的入參是在路由配置中過濾器節(jié)點下面的配置说庭,這樣就可以根據配置做一些特殊的處理然磷,然后再創(chuàng)建實例作為返回值
- StatePrinterGatewayFilterFactory類實現(xiàn)<font color="blue">String name()</font>方法,該方法的返回值就是路由配置文件中過濾器的<font color="red">name</font>
- <font color="blue">String name()</font>也可以不實現(xiàn)刊驴,這是因為定義該方法的接口中有默認實現(xiàn)了姿搜,如下圖,這樣您在路由配置文件中過濾器的<font color="red">name</font>只能是<font color="blue">StatePrinter</font>:
- 在配置文件中捆憎,添加您自定義的過濾器舅柜,該操作和之前的添加內置過濾器一模一樣
以上就是自定義過濾器的基本套路了,可見還是非常簡單的躲惰,接下來的實戰(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注解就能隨意使用了
至此,理論分析已全部完成士葫,問題都已經解決乞而,開始編碼
源碼下載
- 本篇實戰(zhàn)中的完整源碼可在GitHub下載到,地址和鏈接信息如下表所示(https://github.com/zq2599/blog_demos):
名稱 | 鏈接 | 備注 |
---|---|---|
項目主頁 | 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)的自定義過濾器就算完成了节槐,整個過程還是有不少知識點的铜异,咱們來盤點一下:
- 常規(guī)的局部過濾器開發(fā)步驟
- 過濾器執(zhí)行順序的邏輯
- spring的依賴注入和自動裝配
- 斷路器的filter源碼
- java的反射基本功
- 本文與《Spring Cloud Gateway的斷路器(CircuitBreaker)功能》結合,誠意滿滿的帶給您理論結合實戰(zhàn)的體驗币绩,希望能給您學習Spring Cloud Gateway的過程中帶來一些參考缆镣;
你不孤單董瞻,欣宸原創(chuàng)一路相伴
歡迎關注公眾號:程序員欣宸
微信搜索「程序員欣宸」田巴,我是欣宸壹哺,期待與您一同暢游Java世界...
https://github.com/zq2599/blog_demos