一文吃透Spring?Cloud?gateway自定義錯(cuò)誤處理Handler
正文
我們來(lái)學(xué)習(xí)和了解下GatewayExceptionHandler有助于我們處理spring gateway和webFlux中的異常自定義處理。它繼承自ErrorWebExceptionHandler, 類(lèi)關(guān)系圖如下:

通過(guò)類(lèi)上述關(guān)系圖,我們可以看到,DefaultErrorWebExceptionHandler是ErrorWebExceptionHandler的實(shí)現(xiàn)類(lèi),如果我們不自定義類(lèi)異常處理器,系統(tǒng)將自動(dòng)裝配DefaultErrorWebExceptionHandler。
我們就來(lái)庖丁解牛,一步步地來(lái)學(xué)習(xí)下這一組類(lèi)和接口。
AbstractErrorWebExceptionHandler

AbstractErrorWebExceptionHandler實(shí)現(xiàn)了ErrorWebExceptionHandler接口,其中handle方法是其核心方法。我們來(lái)看下其實(shí)現(xiàn)代碼:
public Mono<Void> handle(ServerWebExchange exchange, Throwable throwable) {
// 判斷response是否已經(jīng)提交,并且調(diào)用isDisconnectedClientError方法判斷是否是客戶(hù)端斷開(kāi)連接。
// 如果已經(jīng)斷開(kāi)連接,已經(jīng)無(wú)法將消息發(fā)送給客戶(hù)端,直接Mono.error(throwable)拋出異常。
if (!exchange.getResponse().isCommitted() && !this.isDisconnectedClientError(throwable)) {
// exchange.getAttributes().putIfAbsent(ERROR_INTERNAL_ATTRIBUTE, error);
this.errorAttributes.storeErrorInformation(throwable, exchange);
// 創(chuàng)建request
ServerRequest request = ServerRequest.create(exchange, this.messageReaders);
return this.getRoutingFunction(this.errorAttributes).route(request).switchIfEmpty(Mono.error(throwable)).flatMap((handler) -> {
return handler.handle(request);
}).doOnNext((response) -> {
this.logError(request, response, throwable);
}).flatMap((response) -> {
return this.write(exchange, response);
});
} else {
return Mono.error(throwable);
}
}
通過(guò)Handle方法的代碼我們看出主要是實(shí)現(xiàn)了以下的事項(xiàng):
- 判斷response是否已經(jīng)提交,并且調(diào)用isDisconnectedClientError方法判斷是否是客戶(hù)端斷開(kāi)連接。
- 如果已經(jīng)斷開(kāi)連接,已經(jīng)無(wú)法將消息發(fā)送給客戶(hù)端,直接Mono.error(throwable)拋出異常。
- 將異常信息存儲(chǔ)到exchange中。
- 創(chuàng)建request。
- 獲取路由函數(shù)。
- 調(diào)用路由函數(shù)的handle方法。
- 然后執(zhí)行日志記錄。
- 最后將response寫(xiě)入到exchange中。
isDisconnectedClientError方法
private boolean isDisconnectedClientError(Throwable ex) {
return DISCONNECTED_CLIENT_EXCEPTIONS.contains(ex.getClass().getSimpleName()) ||this.isDisconnectedClientErrorMessage(NestedExceptionUtils.getMostSpecificCause(ex).getMessage());
}
DISCONNECTED_CLIENT_EXCEPTIONS是一個(gè)集合,里面存放了一些異常信息,如果是這些異常信息, 就認(rèn)為是客戶(hù)端斷開(kāi)連接了。 或者通過(guò)isDisconnectedClientErrorMessage方法判斷是否是客戶(hù)端斷開(kāi)連接的異常信息。
下面我們來(lái)看看isDisconnectedClientErrorMessage方法的實(shí)現(xiàn)。
isDisconnectedClientErrorMessage方法:
private boolean isDisconnectedClientErrorMessage(String message) {
message = message != null ? message.toLowerCase() : "";
return message.contains("broken pipe") || message.contains("connection reset by peer");
}
上述代碼的含義是:如果異常信息中包含“broken pipe”或者“connection reset by peer”,就認(rèn)為是客戶(hù)端斷開(kāi)連接了。
小結(jié)
綜合起來(lái),isDisconnectedClientError方法的含義是:如果DISCONNECTED_CLIENT_EXCEPTIONS集合中包含異常信息的類(lèi)名或者異常信息中包含“broken pipe”或者“connection reset by peer”,就認(rèn)為是客戶(hù)端斷開(kāi)連接了。
NestedExceptionUtils
而isDisconnectedClientErrorMessage方法的參數(shù)message來(lái)自于NestedExceptionUtils.getMostSpecificCause(ex).getMessage()。我們進(jìn)一步跟蹤源碼,可以看到NestedExceptionUtils.getMostSpecificCause(ex)方法的源碼如下:
public static Throwable getMostSpecificCause(Throwable ex) {
Throwable cause;
Throwable result = ex;
while(null != (cause = result.getCause()) && (result != cause)) {
result = cause;
}
return result;
}
上述代碼的含義是:如果ex.getCause()不為空,并且ex.getCause()不等于ex,就將ex.getCause()賦值給result,然后繼續(xù)執(zhí)行while循環(huán)。直到ex.getCause()為空或者ex.getCause()等于ex,就返回result。
getRoutingFunction
我們看到獲得路由調(diào)用的是getRoutingFunction方法,而這個(gè)方法是一個(gè)抽象方法,需要在具體的繼承類(lèi)中實(shí)現(xiàn)。
logError
protected void logError(ServerRequest request, ServerResponse response, Throwable throwable) {
if (logger.isDebugEnabled()) {
logger.debug(request.exchange().getLogPrefix() + this.formatError(throwable, request));
}
if (HttpStatus.resolve(response.rawStatusCode()) != null && response.statusCode().equals(HttpStatus.INTERNAL_SERVER_ERROR)) {
logger.error(LogMessage.of(() -> {
return String.format("%s 500 Server Error for %s", request.exchange().getLogPrefix(), this.formatRequest(request));
}), throwable);
}
}
上述代碼的含義:
- 如果是debug級(jí)別的日志,就以debug的方式打印異常信息。
- 如果response的狀態(tài)碼是500,就打印異常信息。
用到的formatError方法就是簡(jiǎn)單的講錯(cuò)誤格式化,代碼不再一一分析。
write
在把內(nèi)容寫(xiě)入的時(shí)候用到了私有方法write,我們看看write方法的代碼如下:
private Mono<? extends Void> write(ServerWebExchange exchange, ServerResponse response) {
exchange.getResponse().getHeaders().setContentType(response.headers().getContentType());
return response.writeTo(exchange, new AbstractErrorWebExceptionHandler.ResponseContext());
}
上述代碼的含義是:將response的contentType設(shè)置到exchange的response中,然后調(diào)用response的writeTo方法,將response寫(xiě)入到exchange中。 ResponseContext是一個(gè)內(nèi)部類(lèi),主要是攜帶了外部類(lèi)中的viewResolvers和messageWriters屬性。
其他的方法
afterPropertiesSet
public void afterPropertiesSet() throws Exception {
if (CollectionUtils.isEmpty(this.messageWriters)) {
throw new IllegalArgumentException("Property 'messageWriters' is required");
}
}
afterPropertiesSet主要是通過(guò)實(shí)現(xiàn)InitializingBean接口,實(shí)現(xiàn)afterPropertiesSet方法,判斷messageWriters是否為空,如果為空就拋出異常。
renderDefaultErrorView
protected Mono<ServerResponse> renderDefaultErrorView(BodyBuilder responseBody, Map<String, Object> error) {
StringBuilder builder = new StringBuilder();
Date timestamp = (Date)error.get("timestamp");
Object message = error.get("message");
Object trace = error.get("trace");
Object requestId = error.get("requestId");
builder.append("<html><body><h2>Whitelabel Error Page</h2>").append("<p>This application has no configured error view, so you are seeing this as a fallback.</p>").append("<div id='created'>").append(timestamp).append("</div>").append("<div>[").append(requestId).append("] There was an unexpected error (type=").append(this.htmlEscape(error.get("error"))).append(", status=").append(this.htmlEscape(error.get("status"))).append(").</div>");
if (message != null) {
builder.append("<div>").append(this.htmlEscape(message)).append("</div>");
}
if (trace != null) {
builder.append("<div style='white-space:pre-wrap;'>").append(this.htmlEscape(trace)).append("</div>");
}
builder.append("</body></html>");
return responseBody.bodyValue(builder.toString());
}
renderDefaultErrorView方法主要是渲染默認(rèn)的錯(cuò)誤頁(yè)面,如果沒(méi)有自定義的錯(cuò)誤頁(yè)面,就會(huì)使用這個(gè)方法渲染默認(rèn)的錯(cuò)誤頁(yè)面。
renderErrorView
protected Mono<ServerResponse> renderErrorView(String viewName, BodyBuilder responseBody, Map<String, Object> error) {
if (this.isTemplateAvailable(viewName)) {
return responseBody.render(viewName, error);
} else {
Resource resource = this.resolveResource(viewName);
return resource != null ? responseBody.body(BodyInserters.fromResource(resource)) : Mono.empty();
}
}
上述代碼的含義是:
- 如果viewName對(duì)應(yīng)的模板存在,就使用模板渲染錯(cuò)誤頁(yè)面。
- 否則就使用靜態(tài)資源渲染錯(cuò)誤頁(yè)面。 在使用資源渲染的時(shí)候,調(diào)用resolveResource方法,代碼如下:
private Resource resolveResource(String viewName) {
// 獲得所有的靜態(tài)資源的路徑
String[] var2 = this.resources.getStaticLocations();
int var3 = var2.length;
// 遍歷所有的靜態(tài)資源
for(int var4 = 0; var4 < var3; ++var4) {
String location = var2[var4];
try {
// 獲得靜態(tài)資源
Resource resource = this.applicationContext.getResource(location);
// 獲得靜態(tài)資源的文件
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return resource;
}
} catch (Exception var7) {
}
}
return null;
}
DefaultErrorWebExceptionHandler
DefaultErrorWebExceptionHandler是ErrorWebExceptionHandler的默認(rèn)實(shí)現(xiàn),繼承了AbstractErrorWebExceptionHandler。通過(guò)對(duì)AbstractErrorWebExceptionHandler的分析,我們知道了大致實(shí)現(xiàn)原理,下面我們來(lái)看看DefaultErrorWebExceptionHandler的具體實(shí)現(xiàn)。
getRoutingFunction
在AbstractErrorWebExceptionHandler中,getRoutingFunction方法是一個(gè)抽象方法,需要子類(lèi)實(shí)現(xiàn)。DefaultErrorWebExceptionHandler的getRoutingFunction方法的實(shí)現(xiàn)如下:
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(this.acceptsTextHtml(), this::renderErrorView).andRoute(RequestPredicates.all(), this::renderErrorResponse);
}
我們看到代碼中調(diào)用了RouterFunctions.route和andRoute方法。RouterFunctions.route的作用是根據(jù)請(qǐng)求的accept頭信息,判斷是否是text/html類(lèi)型,如果是就調(diào)用renderErrorView方法,否則就調(diào)用renderErrorResponse方法。
acceptsTextHtml
protected RequestPredicate acceptsTextHtml() {
return (serverRequest) -> {
try {
// 獲得請(qǐng)求頭中的accept信息
List<MediaType> acceptedMediaTypes = serverRequest.headers().accept();
// 定義一個(gè)MediaType.ALL的MediaType對(duì)象
MediaType var10001 = MediaType.ALL;
// 移除MediaType.ALL
acceptedMediaTypes.removeIf(var10001::equalsTypeAndSubtype);
// 根據(jù)類(lèi)型和權(quán)重對(duì)MediaType進(jìn)行排序
MediaType.sortBySpecificityAndQuality(acceptedMediaTypes);
// 獲得acceptMediaTypes的stream對(duì)象
Stream var10000 = acceptedMediaTypes.stream();
// 獲得MediaType.TEXT_HTML的MediaType對(duì)象
var10001 = MediaType.TEXT_HTML;
var10001.getClass();
// 判斷是否有MediaType.TEXT_HTML
return var10000.anyMatch(var10001::isCompatibleWith);
} catch (InvalidMediaTypeException var2) {
return false;
}
};
}
acceptsTextHtml方法的作用是判斷請(qǐng)求頭中的accept信息是否是text/html類(lèi)型。
renderErrorView
protected Mono<ServerResponse> renderErrorView(ServerRequest request) {
// 通過(guò)getErrorAttributes方法獲得錯(cuò)誤信息
Map<String, Object> error = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML));
// 獲得錯(cuò)誤狀態(tài)碼
int errorStatus = this.getHttpStatus(error);
// 獲得錯(cuò)誤頁(yè)面的模板名稱(chēng)
BodyBuilder responseBody = ServerResponse.status(errorStatus).contentType(TEXT_HTML_UTF8);
// 獲得錯(cuò)誤頁(yè)面的模板名稱(chēng)
return Flux.just(this.getData(errorStatus).toArray(new String[0])).flatMap((viewName) -> {
return this.renderErrorView(viewName, responseBody, error);
}).switchIfEmpty(this.errorProperties.getWhitelabel().isEnabled() ? this.renderDefaultErrorView(responseBody, error) : Mono.error(this.getError(request))).next();
}
其中
this.getData(errorStatus)通過(guò)錯(cuò)誤的code獲得錯(cuò)誤頁(yè)面的名稱(chēng)。- this.renderErrorView(viewName, responseBody, error),通過(guò)錯(cuò)誤頁(yè)面的名稱(chēng),錯(cuò)誤信息,錯(cuò)誤狀態(tài)碼,獲得錯(cuò)誤頁(yè)面的響應(yīng)體。
- this.renderDefaultErrorView(responseBody, error),通過(guò)錯(cuò)誤信息,錯(cuò)誤狀態(tài)碼,獲得默認(rèn)的錯(cuò)誤頁(yè)面的響應(yīng)體。
renderErrorResponse方法
protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
// 通過(guò)getErrorAttributes方法獲得錯(cuò)誤信息
Map<String, Object> error = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL));
// 獲得錯(cuò)誤狀態(tài)碼
// 設(shè)置為json格式的響應(yīng)體
// 返回錯(cuò)誤信息
return ServerResponse.status(this.getHttpStatus(error)).contentType(MediaType.APPLICATION_JSON).body(BodyInserters.fromValue(error));
}
通過(guò)上述代碼看到主要執(zhí)行了以下流程:
- 通過(guò)getErrorAttributes方法獲得錯(cuò)誤信息
- 獲得錯(cuò)誤狀態(tài)碼
- 設(shè)置為json格式的響應(yīng)體
- 返回錯(cuò)誤信息
getErrorAttributeOptions
protected ErrorAttributeOptions getErrorAttributeOptions(ServerRequest request, MediaType mediaType) {
ErrorAttributeOptions options = ErrorAttributeOptions.defaults();
if (this.errorProperties.isIncludeException()) {
options = options.including(new Include[]{Include.EXCEPTION});
}
if (this.isIncludeStackTrace(request, mediaType)) {
options = options.including(new Include[]{Include.STACK_TRACE});
}
if (this.isIncludeMessage(request, mediaType)) {
options = options.including(new Include[]{Include.MESSAGE});
}
if (this.isIncludeBindingErrors(request, mediaType)) {
options = options.including(new Include[]{Include.BINDING_ERRORS});
}
return options;
}
我們看到上述代碼執(zhí)行了下面的流程:
- 獲得ErrorAttributeOptions對(duì)象
- 判斷是否包含異常信息
- 判斷是否包含堆棧信息
- 判斷是否包含錯(cuò)誤信息
- 判斷是否包含綁定錯(cuò)誤信息 如果包含這些信息,就將對(duì)應(yīng)的信息加入到ErrorAttributeOptions對(duì)象中。而這些判斷主要是通過(guò)ErrorProperties對(duì)象中的配置來(lái)判斷的。
自定義自己的異常處理類(lèi)
當(dāng)認(rèn)為默認(rèn)的DefaultErrorWebExceptionHandler不滿(mǎn)足需求時(shí),我們可以自定義自己的異常處理類(lèi)。
繼承DefaultErrorWebExceptionHandler
只需要繼承DefaultErrorWebExceptionHandler類(lèi),然后重寫(xiě)getErrorAttributes方法即可。
import org.springframework.context.annotation.Configuration;
@Configuration
@Order(-1)
public class MyErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {
public MyErrorWebExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, ErrorProperties errorProperties, ApplicationContext applicationContext) {
super(errorAttributes, resourceProperties, errorProperties, applicationContext);
}
@Override
protected Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = super.getErrorAttributes(request, options);
errorAttributes.put("ext", "自定義異常處理類(lèi)");
return errorAttributes;
}
}
繼承AbstractErrorWebExceptionHandler
import org.springframework.context.annotation.Configuration;
@Configuration
@Order(-1)
public class MyErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
public MyErrorWebExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, ErrorProperties errorProperties, ApplicationContext applicationContext) {
super(errorAttributes, resourceProperties, errorProperties, applicationContext);
}
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}
@Override
protected Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = super.getErrorAttributes(request, options);
errorAttributes.put("ext", "自定義異常處理類(lèi)");
return errorAttributes;
}
}
繼承WebExceptionHandler
import org.springframework.context.annotation.Configuration;
@Configuration
@Order(-1)
public class MyErrorWebExceptionHandler implements WebExceptionHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (response.isCommitted()) {
return Mono.error(ex);
} else {
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
return response.writeWith(Mono.just(response.bufferFactory().wrap("自定義異常處理類(lèi)".getBytes())));
}
}
}
總結(jié)
總體工作流程
經(jīng)過(guò)對(duì)AbstractErrorWebExceptionHandler和DefaultErrorWebExceptionHandler中代碼的閱讀和跟蹤。我們不難看出其工作機(jī)制。考慮到有返回API方式的json錯(cuò)誤信息和錯(cuò)誤頁(yè)面方式的錯(cuò)誤信息。所以ExceptionHandler通過(guò)request中的accept的http頭進(jìn)行判斷,如果是json格式則以json的方式進(jìn)行響應(yīng),否則則以錯(cuò)誤頁(yè)面的方式進(jìn)行響應(yīng)。而錯(cuò)誤頁(yè)面的方式,通過(guò)錯(cuò)誤的code獲得錯(cuò)誤頁(yè)面的名稱(chēng),然后通過(guò)錯(cuò)誤頁(yè)面的名稱(chēng),錯(cuò)誤信息,錯(cuò)誤狀態(tài)碼,獲得錯(cuò)誤頁(yè)面的響應(yīng)體。
得到的經(jīng)驗(yàn)
- 異步編程和同步編程之間差異很大,特別是在對(duì)request和response進(jìn)行操作的時(shí)候。
- 源代碼充分考慮各種情況和細(xì)節(jié),在編程中值得我們?nèi)プ鰠⒖肌?/li>
以上就是一文吃透Spring Cloud gateway自定義錯(cuò)誤處理Handler的詳細(xì)內(nèi)容,更多關(guān)于Spring Cloud gateway Handler的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- spring?cloud?gateway中netty線(xiàn)程池小優(yōu)化
- Spring?Cloud?Gateway中netty線(xiàn)程池優(yōu)化示例詳解
- SpringCloudGateway使用Skywalking時(shí)日志打印traceId解析
- SpringCloudGateway?Nacos?GitlabRunner全自動(dòng)灰度服務(wù)搭建發(fā)布
- SpringCloud Gateway動(dòng)態(tài)路由配置詳解
- Spring?Cloud?Gateway動(dòng)態(tài)路由Apollo實(shí)現(xiàn)詳解
- springcloud?gateway實(shí)現(xiàn)簡(jiǎn)易版灰度路由步驟詳解
相關(guān)文章
SpringBoot使用Apache Tika實(shí)現(xiàn)多種文檔的內(nèi)容解析
在日常開(kāi)發(fā)中,我們經(jīng)常需要解析不同類(lèi)型的文檔,如PDF、Word、Excel、HTML、TXT等,Apache Tika是一個(gè)強(qiáng)大的內(nèi)容解析工具,可以輕松地提取文檔中的內(nèi)容和元數(shù)據(jù)信息,本文將通過(guò)SpringBoot和Apache Tika的結(jié)合,介紹如何實(shí)現(xiàn)對(duì)多種文檔格式的內(nèi)容解析2024-12-12
Java優(yōu)先隊(duì)列(PriorityQueue)重寫(xiě)compare操作
這篇文章主要介紹了Java優(yōu)先隊(duì)列(PriorityQueue)重寫(xiě)compare操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-10-10
SpringBoot返回結(jié)果統(tǒng)一處理實(shí)例詳解
這篇文章主要為大家介紹了SpringBoot返回結(jié)果統(tǒng)一處理實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12
Java常用HASH算法總結(jié)【經(jīng)典實(shí)例】
這篇文章主要介紹了Java常用HASH算法,結(jié)合實(shí)例形式總結(jié)分析了Java常用的Hash算法,包括加法hash、旋轉(zhuǎn)hash、FNV算法、RS算法hash、PJW算法、ELF算法、BKDR算法、SDBM算法、DJB算法、DEK算法、AP算法等,需要的朋友可以參考下2017-09-09
關(guān)于IDEA配置Hibernate中遇到的問(wèn)題解決
這篇文章主要給大家介紹了關(guān)于IDEA配置Hibernate中遇到的問(wèn)題,文中通過(guò)圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05
Java負(fù)載均衡策略的實(shí)現(xiàn)詳解
這篇文章主要介紹了Java負(fù)載均衡策略的實(shí)現(xiàn),負(fù)載均衡在Java領(lǐng)域中有著廣泛深入的應(yīng)用,不管是大名鼎鼎的nginx,還是微服務(wù)治理組件如dubbo,feign等,負(fù)載均衡的算法在其中都有著實(shí)際的使用,需要的朋友可以參考下2022-07-07
System.getProperty(user.dir)定位問(wèn)題解析
System.getProperty(user.dir) 獲取的是啟動(dòng)項(xiàng)目的容器位置,用IDEA是項(xiàng)目的根目錄,部署在tomcat上是tomcat的啟動(dòng)路徑,即tomcat/bin的位置,這篇文章主要介紹了System.getProperty(user.dir)定位問(wèn)題,需要的朋友可以參考下2023-05-05
java 快速實(shí)現(xiàn)異步調(diào)用的操作方法
這篇文章主要介紹了java 如何快速實(shí)現(xiàn)異步調(diào)用方法,今天我們就來(lái)了解下 CompletableFuture,它Java 8引入的一種功能強(qiáng)大的異步編程工具,可以用于實(shí)現(xiàn)復(fù)雜的異步操作和處理鏈?zhǔn)降漠惒饺蝿?wù),需要的朋友可以參考下2023-07-07

