spring-cloud-gateway網關自定義異常處理

最近想對SpringCloudGateway的網關請求轉發響應異常信息進行統一的包裝,比如:訪問404,需要返回自定義的JSON格式,替換原來的springWeb錯誤提示內容;

針對springcloudgateway進行了源碼研究,瞭解到了DefaultErrorWebExceptionHandler在gateway中是如何運作的(其實是springboot裏組件)。

源碼解析

其基本運行方式如下:

1.通過HttpHandlerAutoConfiguration自動配置裝載類創建HttpHandler實例Bean,並將其裝配到spring-web的核心生命週期與上下文內;
2.在此httpHandler方法的執行過程中,會調用WebHttpHandlerBuilder.applicationContext(this.applicationContext).build();


3.在WebHttpHandlerBuilder的applicationContext()方法中,會創建相應WbFilters、Handlers進行創建與屬性賦值;
4.其中applicationContext()方法中會從applicationContext上下文中找到所有實現WebExceptionHandler接口的類。


5.在WebHttpHandlerBuilder的build()方法中會創建相應WebHandler,賦值到ExceptionHandlingWebHandler的私有常量集合exceptionHandlers對象中,重要:此集合對象就是管理着網關轉發異常處理器。併爲實例對象HttpWebHandlerAdapter設置各種配置屬性、上下文對象;


6.當gateway執行網關路由轉發後,所有網關轉發發生的異常會先調用HttpWebHandlerAdapter的handle()方法,該方法內又會調用ExceptionHandlingWebHandler類的handle(),此方法會對exceptionHandlers異常集合,進行遍歷執行用於組裝或生成異常響應信息的各種響應結構,如:header、status、context;

exceptionHandlers集合對象通常會有以下幾種處理類(默認按以下順序排列)

  • CheckpointInsertingHandler
  • DefaultErrorWebExceptionHandler(繼承自AbstractErrorWebExceptionHandler)
  • WebFluxResponseStatusExceptionHandler

以上幾種處理器均實現了WebExceptionHandler接口

比如:我們常見的404錯誤信息

Whitelabel Error Page
This application has no configured error view, so you are seeing this as a fallback.
Mon Nov 08 20:53:11 CST 2021
[5877e728-1] There was an unexpected error (type=Not Found, status=404).

就是從AbstractErrorWebExceptionHandler異常處理器renderDefaultErrorView()方法中組裝的內容輸出的;

解決方案
增加自定義全局異常輸出,我方案如下:
1.創建自定異常處理器,同親實現WebExceptionHandler接口,用於在WebHttpHandlerBuilder的applicationContext()方法中將自定義異常處理器加載到exceptionHandlers集合中,實現spring統一管理;


2.因exceptionHandlers集合對象中已經進行了默認順序添加handler異常處理類,自定義處理器類則加到了集合隊列的尾部;默認CheckpointInsertingHandler排在第一索引位;


3.加入尾部會導致無法輸出,因爲在自定義處理器的前面AbstractErrorWebExceptionHandler處理器中會輸出異常內容,從而阻斷了自定義處理器的內容輸出;
4.將自定義處理器類CustomWebExceptionHandler對象添加到exceptionHandlers集合對象最前例,默認進行異常處理器鏈路調用時,最先調用自定義處理器,從而向調用客戶端輸出自定義異常信息;以下是默認的exceptionHandlers集合對象;

5.實現方式在自定義處理器類CustomWebExceptionHandler上添加@Order(-1)註解標籤,並設置排序值爲-1,默認最小值優先級最大;以下添加自定義處理器類CustomWebExceptionHandler的exceptionHandlers集合對象;

實現代碼
類:CustomWebExceptionHandler.java

import com.alibaba.fastjson.JSONObject;
import com.flying.fish.formwork.util.ApiResult;
import com.flying.fish.formwork.util.Constants;
import com.flying.fish.formwork.util.HttpResponseUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.NestedExceptionUtils;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import reactor.core.publisher.Mono;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

@Slf4j
@Order(-1)
@Component
public class CustomWebExceptionHandler implements WebExceptionHandler {

    private static final Set<String> DISCONNECTED_CLIENT_EXCEPTIONS;
    //排除部份系統級的異常
    static {
        Set<String> exceptions = new HashSet<>();
        exceptions.add("AbortedException");
        exceptions.add("ClientAbortException");
        exceptions.add("EOFException");
        exceptions.add("EofException");
        DISCONNECTED_CLIENT_EXCEPTIONS = Collections.unmodifiableSet(exceptions);
    }

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        if (exchange.getResponse().isCommitted() || isDisconnectedClientError(ex)) {
            return Mono.error(ex);
        }
        ServerHttpRequest request = exchange.getRequest();
        String rawQuery = request.getURI().getRawQuery();
        String query = StringUtils.hasText(rawQuery) ? "?" + rawQuery : "";
        String path = request.getPath() + query ;
        String message ;
        HttpStatus status = determineStatus(ex);
        if (status == null){
            status = HttpStatus.INTERNAL_SERVER_ERROR;
        }
        // 通過狀態碼自定義異常信息
        if (status.value() >= 400 && status.value() < 500){
            message = "路由服務不可達或禁止訪問!";
        }else {
            message = "路由服務異常!";
        }
        message += " path:" + path;
        String jsonMsg = JSONObject.toJSONString(new ApiResult(Constants.FAILED, message, null));
        //工具類輸出json字符串
        return HttpResponseUtils.write(exchange.getResponse(), status, jsonMsg);
    }

    @Nullable
    protected HttpStatus determineStatus(Throwable ex) {
        if (ex instanceof ResponseStatusException) {
            return ((ResponseStatusException) ex).getStatus();
        }
        return null;
    }

    private boolean isDisconnectedClientError(Throwable ex) {
        return DISCONNECTED_CLIENT_EXCEPTIONS.contains(ex.getClass().getSimpleName())
                || isDisconnectedClientErrorMessage(NestedExceptionUtils.getMostSpecificCause(ex).getMessage());
    }

    private boolean isDisconnectedClientErrorMessage(String message) {
        message = (message != null) ? message.toLowerCase() : "";
        return (message.contains("broken pipe") || message.contains("connection reset by peer"));
    }
}

通過上述方案,成功實現全局自定義異常捕獲與消息自定義輸出,如:404,輸出爲

{
"code": "0",
"msg": "路由服務不可達或禁止訪問! path:/parent/userCenter/getUser2",
"timestamp": 1636376343775
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章