Spring Cloud Gateway過濾器精確控制異常返回(分析篇)

程式設計師欣宸 發表於 2021-11-25
Spring

歡迎訪問我的GitHub

這裡分類和彙總了欣宸的全部原創(含配套原始碼):https://github.com/zq2599/blog_demos

本篇概覽

  • 《Spring Cloud Gateway修改請求和響應body的內容》一文中,我們們通過filter成功修改請求body的內容,當時留下個問題:在filter中如果發生異常(例如請求引數不合法),丟擲異常資訊的時候,呼叫方收到的返回碼和body都是Spring Cloud Gateway框架處理後的,呼叫方無法根據這些內容知道真正的錯誤原因,如下圖:

在這裡插入圖片描述

  • 本篇任務就是分析上述現象的原因,通過閱讀原始碼搞清楚返回碼和響應body生成的具體邏輯

提前小結

  • 這裡將分析結果提前小結出來,如果您很忙碌沒太多時間卻又想知道最終原因,直接關注以下小結即可:
  1. Spring Cloud Gateway應用中,有個ErrorAttributes型別的bean,它的getErrorAttributes方法返回了一個map
  2. 應用丟擲異常時,返回碼來自上述map的status的值,返回body是整個map序列化的結果
  3. 預設情況下ErrorAttributes的實現類是DefaultErrorAttributes
  • 再看上述map的status值(也就是response的返回碼),在DefaultErrorAttributes是如何生成的:
  1. 先看異常物件是不是ResponseStatusException型別
  2. 如果是ResponseStatusException型別,就呼叫異常物件的getStatus方法作為返回值
  3. 如果不是ResponseStatusException型別,再看異常類有沒有ResponseStatus註解,
  4. 如果有,就取註解的code屬性作為返回值
  5. 如果異常物件既不是ResponseStatusException型別,也沒有ResponseStatus註解,就返回500
  • 最後看map的message欄位(也就是response body的message欄位),在DefaultErrorAttributes是如何生成的:
  1. 異常物件是不是BindingResult型別
  2. 如果不是BindingResult型別,就看是不是ResponseStatusException型別
  3. 如果是,就用getReason作為返回值
  4. 如果也不是ResponseStatusException型別,就看異常類有沒有ResponseStatus註解,如果有就取該註解的reason屬性作為返回值
  5. 如果通過註解取得的reason也無效,就返回異常的getMessage欄位
  • 上述內容就是本篇精華,但是並未包含分析過程,如果您對Spring Cloud原始碼感興趣,請允許欣宸陪伴您來一次短暫的原始碼閱讀之旅

Spring Cloud Gateway錯誤處理原始碼

  • 首先要看的是配置類ErrorWebFluxAutoConfiguration.java,這裡面向spring註冊了兩個例項,每個都非常重要,我們們先關注第一個,也就是說ErrorWebExceptionHandler的實現類是DefaultErrorWebExceptionHandler:

在這裡插入圖片描述

  • 處理異常時,會通過FluxOnErrorResume呼叫到這個ErrorWebExceptionHandler的handle方法處理,該方法在其父類AbstractErrorWebExceptionHandler.java中,如下圖,紅框位置的程式碼是關鍵,異常返回內容就是在這裡決定的:

在這裡插入圖片描述

  • 展開這個getRoutingFunction方法,可見會呼叫renderErrorResponse來處理響應:
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
		return route(acceptsTextHtml(), this::renderErrorView).andRoute(all(), this::renderErrorResponse);
	}
  • 開啟renderErrorResponse方法,如下所示,真相大白了!
protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
  // 取出所有錯誤資訊
  Map<String, Object> error = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
  
  // 構造返回的所有資訊 
  return ServerResponse
           // 控制返回碼
           .status(getHttpStatus(error))
           // 控制返回ContentType
           .contentType(MediaType.APPLICATION_JSON)
           // 控制返回內容
           .body(BodyInserters.fromValue(error));
}
  • 通過上述程式碼,我們們得到兩個重要結論:
  1. 返回給呼叫方的狀態碼,取決於getHttpStatus方法的返回值
  2. 返回給呼叫方的body,取決於error的內容
  • 都已經讀到了這裡,自然要看看getHttpStatus的內部,如下所示,status來自入參:
protected int getHttpStatus(Map<String, Object> errorAttributes) {
  return (int) errorAttributes.get("status");
}
  • 至此,我們們可以得出一個結論:getErrorAttributes方法的返回值是決定返回碼和返回body的關鍵!

  • 來看看這個getErrorAttributes方法的廬山真面吧,在DefaultErrorAttributes.java中(回憶剛才看ErrorWebFluxAutoConfiguration.java的時候,前面曾提到裡面的東西都很重要,也包括errorAttributes方法):

public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
        Map<String, Object> errorAttributes = this.getErrorAttributes(request, options.isIncluded(Include.STACK_TRACE));
        if (Boolean.TRUE.equals(this.includeException)) {
            options = options.including(new Include[]{Include.EXCEPTION});
        }

        if (!options.isIncluded(Include.EXCEPTION)) {
            errorAttributes.remove("exception");
        }

        if (!options.isIncluded(Include.STACK_TRACE)) {
            errorAttributes.remove("trace");
        }

        if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
            errorAttributes.put("message", "");
        }

        if (!options.isIncluded(Include.BINDING_ERRORS)) {
            errorAttributes.remove("errors");
        }

        return errorAttributes;
    }
  • 篇幅所限,就不再展開上述程式碼了,直接上結果吧:
  1. 返回碼來自determineHttpStatus的返回
  2. message欄位來自determineMessage的返回
  • 開啟determineHttpStatus方法,終極答案揭曉,請關注中文註釋:
private HttpStatus determineHttpStatus(Throwable error, MergedAnnotation<ResponseStatus> responseStatusAnnotation) {
        // 異常物件是不是ResponseStatusException型別
        return error instanceof ResponseStatusException 
        // 如果是ResponseStatusException型別,就呼叫異常物件的getStatus方法作為返回值
        ? ((ResponseStatusException)error).getStatus() 
        // 如果不是ResponseStatusException型別,再看異常類有沒有ResponseStatus註解,
        // 如果有,就取註解的code屬性作為返回值
        : (HttpStatus)responseStatusAnnotation.getValue("code", HttpStatus.class)
        // 如果異常物件既不是ResponseStatusException型別,也沒有ResponseStatus註解,就返回500
        .orElse(HttpStatus.INTERNAL_SERVER_ERROR);
    }
  • 另外,message欄位的內容也確定了:
    private String determineMessage(Throwable error, MergedAnnotation<ResponseStatus> responseStatusAnnotation) {
        // 異常物件是不是BindingResult型別
        if (error instanceof BindingResult) {
            // 如果是,就用getMessage作為返回值
            return error.getMessage();
        } 
        // 如果不是BindingResult型別,就看是不是ResponseStatusException型別
        else if (error instanceof ResponseStatusException) {
            // 如果是,就用getReason作為返回值
            return ((ResponseStatusException)error).getReason();
        } else {
            // 如果也不是ResponseStatusException型別,
            // 就看異常類有沒有ResponseStatus註解,如果有就取該註解的reason屬性作為返回值
            String reason = (String)responseStatusAnnotation.getValue("reason", String.class).orElse("");
            if (StringUtils.hasText(reason)) {
                return reason;
            } else {
                // 如果通過註解取得的reason也無效,就返回異常的getMessage欄位
                return error.getMessage() != null ? error.getMessage() : "";
            }
        }
    }
  • 至此,原始碼分析已完成,最終的返回碼和返回內容究竟如何控制,相信聰明的您心裡應該有數了,下一篇《實戰篇》我們們趁熱打鐵,寫程式碼試試精確控制返回碼和返回內容

  • 提前劇透,接下來的《實戰篇》會有以下內容呈現:

  1. 直接了當,控制返回碼和body中的error欄位
  2. 小小攔路虎,見招拆招
  3. 簡單易用,通過註解控制返回資訊
  4. 終極方案,完全定製返回內容
  • 以上內容敬請期待,欣宸原創必不辜負您

你不孤單,欣宸原創一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 資料庫+中介軟體系列
  6. DevOps系列

歡迎關注公眾號:程式設計師欣宸

微信搜尋「程式設計師欣宸」,我是欣宸,期待與您一同暢遊Java世界...
https://github.com/zq2599/blog_demos