歡迎訪問我的GitHub
這裡分類和彙總了欣宸的全部原創(含配套原始碼):https://github.com/zq2599/blog_demos
本篇概覽
- 在《Spring Cloud Gateway修改請求和響應body的內容》一文中,我們們通過filter成功修改請求body的內容,當時留下個問題:在filter中如果發生異常(例如請求引數不合法),丟擲異常資訊的時候,呼叫方收到的返回碼和body都是Spring Cloud Gateway框架處理後的,呼叫方無法根據這些內容知道真正的錯誤原因,如下圖:
- 本篇任務就是分析上述現象的原因,通過閱讀原始碼搞清楚返回碼和響應body生成的具體邏輯
提前小結
- 這裡將分析結果提前小結出來,如果您很忙碌沒太多時間卻又想知道最終原因,直接關注以下小結即可:
- Spring Cloud Gateway應用中,有個ErrorAttributes型別的bean,它的getErrorAttributes方法返回了一個map
- 應用丟擲異常時,返回碼來自上述map的status的值,返回body是整個map序列化的結果
- 預設情況下ErrorAttributes的實現類是DefaultErrorAttributes
- 再看上述map的status值(也就是response的返回碼),在DefaultErrorAttributes是如何生成的:
- 先看異常物件是不是ResponseStatusException型別
- 如果是ResponseStatusException型別,就呼叫異常物件的getStatus方法作為返回值
- 如果不是ResponseStatusException型別,再看異常類有沒有ResponseStatus註解,
- 如果有,就取註解的code屬性作為返回值
- 如果異常物件既不是ResponseStatusException型別,也沒有ResponseStatus註解,就返回500
- 最後看map的message欄位(也就是response body的message欄位),在DefaultErrorAttributes是如何生成的:
- 異常物件是不是BindingResult型別
- 如果不是BindingResult型別,就看是不是ResponseStatusException型別
- 如果是,就用getReason作為返回值
- 如果也不是ResponseStatusException型別,就看異常類有沒有ResponseStatus註解,如果有就取該註解的reason屬性作為返回值
- 如果通過註解取得的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));
}
- 通過上述程式碼,我們們得到兩個重要結論:
- 返回給呼叫方的狀態碼,取決於getHttpStatus方法的返回值
- 返回給呼叫方的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;
}
- 篇幅所限,就不再展開上述程式碼了,直接上結果吧:
- 返回碼來自determineHttpStatus的返回
- 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() : "";
}
}
}
-
至此,原始碼分析已完成,最終的返回碼和返回內容究竟如何控制,相信聰明的您心裡應該有數了,下一篇《實戰篇》我們們趁熱打鐵,寫程式碼試試精確控制返回碼和返回內容
-
提前劇透,接下來的《實戰篇》會有以下內容呈現:
- 直接了當,控制返回碼和body中的error欄位
- 小小攔路虎,見招拆招
- 簡單易用,通過註解控制返回資訊
- 終極方案,完全定製返回內容
- 以上內容敬請期待,欣宸原創必不辜負您
你不孤單,欣宸原創一路相伴
歡迎關注公眾號:程式設計師欣宸
微信搜尋「程式設計師欣宸」,我是欣宸,期待與您一同暢遊Java世界...
https://github.com/zq2599/blog_demos