歡迎訪問我的GitHub
https://github.com/zq2599/blog_demos
內容:所有原創文章分類彙總及配套原始碼,涉及Java、Docker、Kubernetes、DevOPS等;
本篇概覽
-
本文是《Spring Cloud Gateway實戰》系列的第七篇,前面的文章我們們學習了各種內建過濾器,還在《Spring Cloud Gateway的斷路器(CircuitBreaker)功能》一文深入研究了斷路器型別的過濾器(理論&實戰&原始碼分析皆有),相信聰明的您一定會有此疑問:內建的再多也無法覆蓋全部場景,定製才是終極武器
-
所以今天我們們就來開發一個自己專屬的過濾器,至於此過濾器的具體功能,其實前文已埋下伏筆,如下圖:
-
簡單來說,就是在一個有斷路器的Spring Cloud Gateway應用中做個自定義過濾器,在處理每個請求時把斷路器的狀態列印出來,這樣我們們就能明明白白清清楚楚知道斷路器的狀態啥時候改變,變成了啥樣,也算補全了《Spring Cloud Gateway的斷路器(CircuitBreaker)功能》的知識點
-
過濾器分為全域性和區域性兩種,這裡我們們選用區域性的,原因很簡單:我們們的過濾器是為了觀察斷路器,所以不需要全域性生效,只要在使用斷路器的路由中生效就夠了;
套路提前知曉
- 我們們先看看自定義區域性過濾器的的基本套路:
- 新建一個類(我這裡名為StatePrinterGatewayFilter.java),實現GatewayFilter和Ordered介面,重點是filter方法,該過濾器的主要功能就在這裡面實現
- 新建一個類(我這裡名為StatePrinterGatewayFilterFactory.java),實現AbstractGatewayFilterFactory方法,其apply方法的返回值就是上一步新建的StatePrinterGatewayFilter的例項,該方法的入參是在路由配置中過濾器節點下面的配置,這樣就可以根據配置做一些特殊的處理,然後再建立例項作為返回值
- StatePrinterGatewayFilterFactory類實現String name()方法,該方法的返回值就是路由配置檔案中過濾器的name
- String name()也可以不實現,這是因為定義該方法的介面中有預設實現了,如下圖,這樣您在路由配置檔案中過濾器的name只能是StatePrinter:
- 在配置檔案中,新增您自定義的過濾器,該操作和之前的新增內建過濾器一模一樣
-
以上就是自定義過濾器的基本套路了,可見還是非常簡單的,接下來的實戰也是按照這個套路來的
-
在編寫自定義過濾器程式碼之前,還有個攔路虎等著我們,也就是我們們過濾器的基本功能:如何取得斷路器的狀態
如何取得斷路器的狀態
- 前文的程式碼分析中,我們們瞭解到斷路器的核心功能集中在SpringCloudCircuitBreakerFilterFactory.apply方法中(沒錯,就是剛才提到的apply方法),開啟這個類,如下圖,從綠框可見斷路器功能來自名為cb的物件,而這個物件是在紅框處由reactiveCircuitBreakerFactory建立的:
- 展開上圖紅框右側的reactiveCircuitBreakerFactory.create方法繼續看,最終跟蹤到了ReactiveResilience4JCircuitBreakerFactory類,發現了一個極其重要的變數,就是下圖紅框中的circuitBreakerRegistry,它的內部有個ConcurrentHashMap(InMemoryRegistryStore的entryMap),這裡面存放了所有斷路器例項:
- 此時您應該想到了,拿到斷路器的關鍵就是拿到上圖紅框中的circuitBreakerRegistry物件,不過怎麼拿呢?首先它是私有型別的,其次雖然有個方法返回了該物件,但是此方法並非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次呼叫才能開始進行統計計算
.failureRateThreshold(50) // 在單位時間視窗內呼叫失敗率達到50%後會啟動斷路器
.enableAutomaticTransitionFromOpenToHalfOpen() // 允許斷路器自動由開啟狀態轉換為半開狀態
.permittedNumberOfCallsInHalfOpenState(5) // 在半開狀態下允許進行正常呼叫的次數
.waitDurationInOpenState(Duration.ofSeconds(5)) // 斷路器開啟狀態轉換為半開狀態需要等待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註解就能隨意使用了
-
至此,理論分析已全部完成,問題都已經解決,開始編碼
原始碼下載
- 本篇實戰中的完整原始碼可在GitHub下載到,地址和連結資訊如下表所示(https://github.com/zq2599/blog_demos):
名稱 | 連結 | 備註 |
---|---|---|
專案主頁 | https://github.com/zq2599/blog_demos | 該專案在GitHub上的主頁 |
git倉庫地址(https) | https://github.com/zq2599/blog_demos.git | 該專案原始碼的倉庫地址,https協議 |
git倉庫地址(ssh) | git@github.com:zq2599/blog_demos.git | 該專案原始碼的倉庫地址,ssh協議 |
- 這個git專案中有多個資料夾,本篇的原始碼在spring-cloud-tutorials資料夾下,如下圖紅框所示:
- spring-cloud-tutorials資料夾下有多個子工程,本篇的程式碼是circuitbreaker-gateway,如下圖紅框所示:
編碼
-
前文建立了子工程circuitbreaker-gateway,此工程已新增了斷路器,現在我們們的過濾器程式碼就寫在這個工程中是最合適的了
-
接下來按照套路寫程式碼,首先是StatePrinterGatewayFilter.java,程式碼中有詳細註釋就不再囉嗦了,要注意的是getOrder方法返回值是10,這表示過濾器的執行順序:
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) {
// 這裡沒有考慮併發的情況,如果是生產環境,請您自行新增上鎖的邏輯
if (null==circuitBreaker) {
CircuitBreakerRegistry circuitBreakerRegistry = null;
try {
Method method = reactiveResilience4JCircuitBreakerFactory.getClass().getDeclaredMethod("getCircuitBreakerRegistry",(Class[]) null);
// 用反射將getCircuitBreakerRegistry方法設定為可訪問
method.setAccessible(true);
// 用反射執行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();
}
// 取斷路器狀態,再判空一次,因為上面的操作未必能取到circuitBreaker
String state = (null==circuitBreaker) ? "unknown" : circuitBreaker.getState().name();
System.out.println("state : " + state);
// 繼續執行後面的邏輯
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,如下圖紅框所示,斷路器狀態已經列印出來,至此,我們可以精確把握斷路器的狀態變化了:
分析請求被filter漏掉的問題
-
有個很明顯的問題,聰明睿智的您當然不會忽略:上圖綠框中的連續四個響應,對應的斷路器狀態都沒有列印出來,要知道,我們們的過濾器可是要處理每一個請求的,怎麼會連續漏掉四個呢?
-
其實原因很容易推理出來:斷路器CircuitBreaker的filter先執行,然後才是我們們的CircuitBreakerStatePrinter,而處於開啟狀態的斷路器會直接返回錯誤給呼叫方,其後面的filter都不會執行了
-
那麼問題來了:如何控制CircuitBreaker和CircuitBreakerStatePrinter這兩個filter的順序,讓CircuitBreakerStatePrinter先執行?
-
CircuitBreakerStatePrinter是我們們自己寫的程式碼,修改StatePrinterGatewayFilter.getOrder的返回值可以調整順序,但CircuitBreaker不是我們自己的程式碼呀,這可如何是好?
-
老規矩,看看斷路器的原始碼,前文已經分析過了,斷路器最重要的程式碼是SpringCloudCircuitBreakerFilterFactory.apply方法,如下圖紅框,生成的filter是GatewayFilter介面的實現類:
- 再看載入過濾器到集合的那段關鍵程式碼,在RouteDefinitionRouteLocator.loadGatewayFilters方法中,如下圖所示,由於CircuitBreaker的filter並沒有實現Ordered介面,因此執行的是紅框中的程式碼,代表其順序的值等於i+1,這個i就是遍歷路由配置中所有過濾器時的一個從零開始的自增變數而已:
-
回顧我們們的路由配置,CircuitBreaker在前,CircuitBreakerStatePrinter在後,所以,在新增CircuitBreaker的時候,i等於0,那麼CircuitBreaker的order就等於i+1=1了
-
而CircuitBreakerStatePrinter實現了Ordered介面,因此不會走紅框中的程式碼,其order等於我們們寫在程式碼中的值,我們們寫的是10
-
所以:CircuitBreaker的order等於1,CircuitBreakerStatePrinter等於10,當然是CircuitBreaker先執行了!
再次修改
-
知道了原因,改起來就容易了,我的做法很簡單:StatePrinterGatewayFilter不再實現Ordered,這樣就和CircuitBreaker的filter一樣,執行的是上圖紅框中的程式碼,這樣,在配置檔案中,誰放在前面誰就先執行
-
程式碼就不貼出來了,您自行刪除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,如下圖,這一次,每個請求都會列印出此時斷路器的狀態:
知識點小結
- 至此,用於觀測斷路器狀態的自定義過濾器就算完成了,整個過程還是有不少知識點的,我們們來盤點一下:
- 常規的區域性過濾器開發步驟
- 過濾器執行順序的邏輯
- spring的依賴注入和自動裝配
- 斷路器的filter原始碼
- java的反射基本功
- 本文與《Spring Cloud Gateway的斷路器(CircuitBreaker)功能》結合,誠意滿滿的帶給您理論結合實戰的體驗,希望能給您學習Spring Cloud Gateway的過程中帶來一些參考;
你不孤單,欣宸原創一路相伴
歡迎關注公眾號:程式設計師欣宸
微信搜尋「程式設計師欣宸」,我是欣宸,期待與您一同暢遊Java世界...
https://github.com/zq2599/blog_demos