(WebFlux)004、WebFilter踩坑記錄

来源:https://www.cnblogs.com/lifacheng/archive/2022/09/28/16739743.html
-Advertisement-
Play Games

一、背景 使用SpringWebFlux的WebFilter時,由於不熟悉或一些思考疏忽,容易出現未知的異常。記錄一下排查與解決方案,給大家分享一下。 二、問題 2.1 問題描述 在測試介面方法時,出現的錯誤信息如下(對一些項目路徑做了修改): java.lang.IllegalStateExcep ...


一、背景

使用SpringWebFlux的WebFilter時,由於不熟悉或一些思考疏忽,容易出現未知的異常。記錄一下排查與解決方案,給大家分享一下。

二、問題

2.1 問題描述

在測試介面方法時,出現的錯誤信息如下(對一些項目路徑做了修改):

java.lang.IllegalStateException: COMPLETED
	at org.springframework.http.server.reactive.AbstractListenerReadPublisher$State.subscribe(AbstractListenerReadPublisher.java:451)
	Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
	*__checkpoint ⇢ springfox.boot.starter.autoconfigure.SwaggerUiWebFluxConfiguration$CustomWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ com.xxx.config.LoginWebFilter$$EnhancerBySpringCGLIB$$f3da6bdf [DefaultWebFilterChain]
	*__checkpoint ⇢ com.xxx.config.TraceIdFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ HTTP POST "/abc/test/testMethod" [ExceptionHandlingWebHandler]
Original Stack Trace:
		at org.springframework.http.server.reactive.AbstractListenerReadPublisher$State.subscribe(AbstractListenerReadPublisher.java:451)
		at org.springframework.http.server.reactive.AbstractListenerReadPublisher.subscribe(AbstractListenerReadPublisher.java:105)

2.2 解決問題

通過查看錯誤信息描述,checkpoint點都在webfilter中,由於對webflux也不是特別熟,所以就只有一個個測試。

通過一系列操作, 把swagger移除,細讀TraceIdFilter(內容不多),主要歸功於原方案是正確的,修改後錯誤,最後才定位問題出現在LoginWebFilter。

說說插曲,原實現方式(有阻塞邏輯,沒出現上述異常),代碼如下:

@Configuration
@Slf4j
@Order(-10)
public class LoginWebFilter implements WebFilter {
    // 略...

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();

        if (!enableGateway) {
            String token = Optional.ofNullable(request.getHeaders().getFirst(Constants.TOKEN))
                    .orElse("");
            // 獲取用戶信息
            User user = getUser(token);
            if (user != null) {
                ServerHttpRequest mutateRequest = exchange.getRequest().mutate()
                        .build();
                exchange = exchange.mutate().request(mutateRequest).build();
            }
        }
        return chain.filter(exchange);
    }

    private User getUser(String token) {
        if (StringUtils.isNotBlank(token)) {
            return redisTemplate.opsForValue().get("xxx:tk:" + token)
                    .flatMap(str -> Mono.justOrEmpty(JsonUtils.toObj(str, User.class))).block();
        }
        return null;
    }
}

這樣寫,沒有複雜的業務邏輯,從上到下,完全OJBK,但是調整後,就出現了上述異常。

改完後的問題代碼如下:

// 錯誤
public class LoginWebFilter implements WebFilter {
	/...略
    @Autowired
    private ReactiveStringRedisTemplate redisTemplate;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {

        if (!enableGateway) {
            ServerHttpRequest request = exchange.getRequest();
            String token = Optional.ofNullable(request.getHeaders().getFirst(Constants.TOKEN))
                    .orElse("");

            return getUser(token).flatMap(user -> {
                ServerHttpRequest mutateRequest = exchange.getRequest().mutate()
                        .header(UserUtils.MEMBER_ID, user.getMemId())
                        .header(UserUtils.MOBILE, user.getMobile())
                        .build();

                ServerWebExchange newexchange = exchange.mutate().request(mutateRequest).build();
                return chain.filter(newexchange);
               // 問題點 
            }).switchIfEmpty(chain.filter(exchange));
        }
        return chain.filter(exchange);
    }
	// 不在用block
    private Mono<User> getUser(String token) {
        if (StringUtils.isNotBlank(token)) {
            return redisTemplate.opsForValue().get("xxx:tk:" + token)
                    .flatMap(str -> Mono.justOrEmpty(JsonUtils.toObj(str, User.class)));
        }
        return Mono.empty();
    }
}

2.3 如何解決

對比改造前和改造後的代碼,其實差異不大,那問題出現在哪呢?

由於對webflux也不是特別熟,那就只能一點點試(太蠢了)。 最後發現問題出現在了switchIfEmpty(chain.filter(exchange)),在去掉了switchIfEmpty(chain.filter(exchange)),就不會在出現上述異常。

修改後部分代碼如下:

// 半正確
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {

    if (!enableGateway) {

        ServerHttpRequest request = exchange.getRequest();
        String token = Optional.ofNullable(request.getHeaders().getFirst(Constants.TOKEN))
            .orElse(“”);

        return getUser(token).flatMap(user -> {
            ServerHttpRequest mutateRequest = exchange.getRequest().mutate()
                .header(UserUtils.MEMBER_ID, user.getMemId())
                .header(UserUtils.MOBILE, user.getMobile())
                .build();

            ServerWebExchange newexchange = exchange.mutate().request(mutateRequest).build();
            return chain.filter(newexchange);
        });
    }
    return chain.filter(exchange);
}

雖然現在不回在出現異常,但是去掉switchIfEmpty後,代碼邏輯是不完整的,當獲取不到User時,返回Mono.emtpy,那會直接結束流程,不在執行剩下的filter或其他邏輯。真是連環坑,一坑接一坑。所以對代碼需要調整一番,調整後如下:

// 有點正確 但是不多
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {

    if (!enableGateway) {

        ServerHttpRequest request = exchange.getRequest();
        String token = Optional.ofNullable(request.getHeaders().getFirst(Constants.TOKEN))
            .orElse(“”);

        return getUser(token).switchIfEmpty(Mono.error(() -> new BizException(ErrorCode.USER_IS_NULL_ERROR)))
            .flatMap(user -> {
                ServerHttpRequest mutateRequest = exchange.getRequest().mutate()
                    .header(UserUtils.MEMBER_ID, user.getMemId())
                    .header(UserUtils.MOBILE, user.getMobile())
                    .build();

                ServerWebExchange newexchange = exchange.mutate().request(mutateRequest).build();
                return chain.filter(newexchange);
            }).onErrorResume(e -> chain.filter(exchange));

    }
    return chain.filter(exchange);
}

當獲取用戶為空後,拋出異常,然後在兜底,當異常的時候執行chain.filter(exchange)(好蠢的方式.. 但是解決問題了)。

2.4 意外之喜

各位看官,就在我寫完上完上面的代碼修改方案之後,讀了一下修改完後的代碼,突然發現問題出在哪了,所以連夜修改了代碼方式。現在我聽我細細道來。

2.4.1 問題點

原因點chain.filter(exchange)重覆執行

switchIfEmpty(chain.filter(exchange))這個點本意是想用在當getUser 方法為空時,執行其它WebFilter的邏輯,從而不影響主流程。

忽略了一點是:當chain.filter(newexchange)這個方法執行完後,返回的也是Mono<Void>,也是為空。所以無論如何,代碼最後的邏輯都會走到switchIfEmpty(chain.filter(exchange))

但是當getUser獲取到用戶後,會重覆執行chain.filter(exchange),如下

  • return chain.filter(newexchange)
  • switchIfEmpty(chain.filter(exchange))

由於第一次執行完chain.filter(exchange),request、response都已經關閉,所以出現了xx COMPLETE,那看來的確符合邏輯。

2.4.2 驗證猜想

這個驗證方式還是挺簡單的,那就是分別傳入正常的TOKEN和錯誤的TOKEN。

具體操作:.....(本人已完成)

結論:

當傳入錯誤的token的時候,確實沒有拋出異常,完美執行。但是當傳入正確的token,出現了熟悉的異常。

2.4.3 代碼調整

知道問題的原因,那就好調整代碼了。修改後如下:

public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
    if (!enableGateway) {
        ServerHttpRequest request = exchange.getRequest();
        String token = Optional.ofNullable(request.getHeaders().getFirst(Constants.TOKEN))
            .orElse(request.getHeaders().getFirst("suuid"));

        return getUser(token).map(user -> {
            ServerHttpRequest mutateRequest = exchange.getRequest().mutate()
                .header(UserUtils.MEMBER_ID, user.getMemId())
                .header(UserUtils.MOBILE, user.getMobile())
                .build();
            return exchange.mutate().request(mutateRequest).build();
            // 調整當getUser為空時,返回的內容
        }).switchIfEmpty(Mono.just(exchange)).flatMap(chain::filter);
        
    }
    return chain.filter(exchange);
}

至此,問題就完全解決拉!心裡美滋滋!

三、總結

1、遇到問題,還是要多看看呀,細細思考一下

2、多看代碼,發現問題,實現完美的解決方案

你的每一個點贊,我都當做喜歡
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 什麼是JavaScript? 前言 本文內容為 博主閱讀“紅寶書”之後的總結和個人理解,有什麼錯誤歡迎指正! 一句話概括語言的誕生 1995年,網景公司一位名叫Brendan Eich的工程師,開發了一個叫Mocha的腳本語言。後來改名叫 JavaScript,以便蹭當時大火的Java的熱度。 到底 ...
  • 方法:定位,外邊距,內邊距,層級,邊框; 一個元素; 兩個元素; 三個元素. <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=e ...
  • 輸出100個hello world. for (var i = 1; i <= 100; i++) { console.log("hello world");} 創建一個包含1~100的數組. var array = [];for (var i = 1; i <= 100; i++) { array ...
  • @(vue2.x引入threejs) vue2.x引入threejs npm安裝 npm install three 使用指定版本: npm install three@<版本號> 其他插件 因為本次開發需要引入3D模型,所以需要使用 MTLLoader, OBJLoader兩種載入器,因為開發需求 ...
  • #背景 學習前端新框架、新技術。如果需要做一些資料庫的操作來增加demo的體驗(CURD流程可以讓演示的體驗根據絲滑) 最開始的時候一個演示程式我們會調用後臺,這樣其實有一點弊端,就是增加了開發和維護成本,簡單的一個demo不應該勞師動眾 後來我會在demo中使用一些websql,奈何,websql ...
  • 裝飾器 (1)什麼是裝飾器: 器指的是工具,可以定義成函數 裝飾指的是為其他事務添加額外的東西來點綴 上面兩者合到一起: 裝飾器指的是定義一個函數,該函數用來為其他函數添加額外的功能 函數裝飾器分為: 無參裝飾器和有參裝飾兩種,二者的實現原理一樣,都是’函數嵌套+閉包+函數對象’的組合使用的產物。 ...
  • 自動化流水線在CI/CD(持續集成/持續交付或持續部署)的實踐中發揮著核心作用。本文將對什麼是CI/CD流水線、如何構建CI/CD流水線進行討論。 *持續集成:Continuous Integration *持續交付:Continuous Delivery *持續部署:Continuous Depl ...
  • 當前,全球汽車產業正在經歷從傳統工業向數字化轉型的大變革,智能化、數字化、信息化正在成為汽車電子行業轉型發展的必由之路。“軟體定義汽車”(Software Defined Vehicles,SDV)概念的提出,說明軟體在汽車產品中承擔的角色越來越重要。隨著汽車軟體的量級和複雜度不斷提高,汽車廠商對嵌 ...
一周排行
    -Advertisement-
    Play Games
  • 1. 說明 /* Performs operations on System.String instances that contain file or directory path information. These operations are performed in a cross-pla ...
  • 視頻地址:【WebApi+Vue3從0到1搭建《許可權管理系統》系列視頻:搭建JWT系統鑒權-嗶哩嗶哩】 https://b23.tv/R6cOcDO qq群:801913255 一、在appsettings.json中設置鑒權屬性 /*jwt鑒權*/ "JwtSetting": { "Issuer" ...
  • 引言 集成測試可在包含應用支持基礎結構(如資料庫、文件系統和網路)的級別上確保應用組件功能正常。 ASP.NET Core 通過將單元測試框架與測試 Web 主機和記憶體中測試伺服器結合使用來支持集成測試。 簡介 集成測試與單元測試相比,能夠在更廣泛的級別上評估應用的組件,確認多個組件一起工作以生成預 ...
  • 在.NET Emit編程中,我們探討了運算操作指令的重要性和應用。這些指令包括各種數學運算、位操作和比較操作,能夠在動態生成的代碼中實現對數據的處理和操作。通過這些指令,開發人員可以靈活地進行算術運算、邏輯運算和比較操作,從而實現各種複雜的演算法和邏輯......本篇之後,將進入第七部分:實戰項目 ...
  • 前言 多表頭表格是一個常見的業務需求,然而WPF中卻沒有預設實現這個功能,得益於WPF強大的控制項模板設計,我們可以通過修改控制項模板的方式自己實現它。 一、需求分析 下圖為一個典型的統計表格,統計1-12月的數據。 此時我們有一個需求,需要將月份按季度劃分,以便能夠直觀地看到季度統計數據,以下為該需求 ...
  • 如何將 ASP.NET Core MVC 項目的視圖分離到另一個項目 在當下這個年代 SPA 已是主流,人們早已忘記了 MVC 以及 Razor 的故事。但是在某些場景下 SSR 還是有意想不到效果。比如某些靜態頁面,比如追求首屏載入速度的時候。最近在項目中回歸傳統效果還是不錯。 有的時候我們希望將 ...
  • System.AggregateException: 發生一個或多個錯誤。 > Microsoft.WebTools.Shared.Exceptions.WebToolsException: 生成失敗。檢查輸出視窗瞭解更多詳細信息。 內部異常堆棧跟蹤的結尾 > (內部異常 #0) Microsoft ...
  • 引言 在上一章節我們實戰了在Asp.Net Core中的項目實戰,這一章節講解一下如何測試Asp.Net Core的中間件。 TestServer 還記得我們在集成測試中提供的TestServer嗎? TestServer 是由 Microsoft.AspNetCore.TestHost 包提供的。 ...
  • 在發現結果為真的WHEN子句時,CASE表達式的真假值判斷會終止,剩餘的WHEN子句會被忽略: CASE WHEN col_1 IN ('a', 'b') THEN '第一' WHEN col_1 IN ('a') THEN '第二' ELSE '其他' END 註意: 統一各分支返回的數據類型. ...
  • 在C#編程世界中,語法的精妙之處往往體現在那些看似微小卻極具影響力的符號與結構之中。其中,“_ =” 這一組合突然出現還真不知道什麼意思。本文將深入剖析“_ =” 的含義、工作原理及其在實際編程中的廣泛應用,揭示其作為C#語法奇兵的重要角色。 一、下劃線 _:神秘的棄元符號 下劃線 _ 在C#中並非 ...