最近想对 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
}