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
}

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