【深入解析spring cloud gateway】07 自定义异常返回报文

Servlet的HttpResponse对象,返回响应报文,一般是这么写的,通过输出流直接就可以将返回报文输出。

java 复制代码
OutputStream out = response.getOutputStream();
out.write("输出的内容");
out.flush();

在filter中如果发生异常(例如请求参数不合法),抛出异常信息的时候,调用方收到的返回码和body都是Spring Cloud Gateway框架处理来处理的。这一节我们分析一下,gateway的异常返回报文是怎么返回的,并定义一个自己的异常返回报文格式。

一、先定义一个Filter,直接抛出异常

定义一个直接抛出异常的filter

java 复制代码
public class ExceptionFilter implements GlobalFilter, Ordered {

    @Override    
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        throw new IllegalArgumentException("参数不合法");    }

    @Override    
    public int getOrder() {
        return 0;    
    }
}

异常抛出如下图

json内容如下:

json 复制代码
{
    "timestamp": "2023-08-28T03:55:02.380+00:00",
    "path": "/hello-service/hello",
    "status": 500,
    "error": "Internal Server Error",
    "requestId": "0204dca5-1"
}

二、源码分析

上节我们分析了核心流程。在整个核心流程中,我们并没有关注有异常的情况。

入口HttpWebHandlerAdapter调用的delegate实际上就是:DefaultErrorWebExceptionHandler

代码如下:

java 复制代码
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
    //省略部分代码
    return getDelegate().handle(exchange)
            .doOnSuccess(aVoid -> logResponse(exchange))
            .onErrorResume(ex -> handleUnresolvedError(exchange, ex))
            .then(Mono.defer(response::setComplete));
}

进入DefaultErrorWebExceptionHandler的handle方法,分析见注释

java 复制代码
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
    Mono<Void> completion;
    try {
 //正常的处理流程
        completion = super.handle(exchange);
    }
    catch (Throwable ex) {
        completion = Mono.error(ex);
    }
//产生异常的情况,由异常处理器来进行处理
    for (WebExceptionHandler handler : this.exceptionHandlers) {
        completion = completion.onErrorResume(ex -> handler.handle(exchange, ex));
    }
    return completion;
}

如果产生异常的情况,由异常处理器来进行处理,这个异常处理器是一个列表。

而异常处理器最核心的就是这个:DefaultErrorWebExceptionHandler

其handle方法如下

java 复制代码
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable throwable) {
    if (exchange.getResponse().isCommitted() || isDisconnectedClientError(throwable)) {
        return Mono.error(throwable);
    }
    this.errorAttributes.storeErrorInformation(throwable, exchange);
    ServerRequest request = ServerRequest.create(exchange, this.messageReaders);
    return getRoutingFunction(this.errorAttributes).route(request)
        .switchIfEmpty(Mono.error(throwable))
        .flatMap((handler) -> handler.handle(request))
        .doOnNext((response) -> logError(request, response, throwable))
        .flatMap((response) -> write(exchange, response));
}

跟到getRoutingFunction里面看看

java 复制代码
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
    return route(acceptsTextHtml(), this::renderErrorView).andRoute(all(), this::renderErrorResponse);
}

最终跟到下面的这个方法:renderErrorResponse,从下面的截图可以看到,error Map这个对象,正是报文体的格式

如果我们想自定义一个异常响应的返回报文,如下,应该怎么弄呢?

json 复制代码
{
    "returnCode": "ERROR",
    "errorMsg": "参数异常",
    "body": null
}

我们实际上可以继承DefaultErrorWebExceptionHandler,并且实现其renderErrorResponse方法就可以了。

可以看到DefaultErrorWebExceptionHandler,是通过下面的方式注入到容器的,如果我们也定义一个也注册到容器,那么就会覆盖原有的实现

整体流程图如下:

三、自定义异常处理器

1、定义一个产生异常的filter,模拟产生异常

java 复制代码
@Slf4j
public class ExceptionFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("filter产生了异常");
        throw new IllegalArgumentException("参数不合法");
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

2、自定义异常处理器

java 复制代码
/**
 * 自定义异常处理器
 */
public class CustomErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {

    public CustomErrorWebExceptionHandler(ErrorAttributes errorAttributes, WebProperties.Resources resources, ErrorProperties errorProperties, ApplicationContext applicationContext) {
        super(errorAttributes, resources, errorProperties, applicationContext);
    }

    @Override
    protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
        // 最终是用responseBodyMap来生成响应body的
        Map<String, Object> responseBodyMap = new HashMap<>();

        // 这里和父类的做法一样,取得DefaultErrorAttributes整理出来的所有异常信息
        Map<String, Object> error = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));

        // 原始的异常信息可以用getError方法取得
        Throwable throwable = getError(request);

        responseBodyMap.put("returnCode", "my error code");
        responseBodyMap.put("errorMsg", throwable.getMessage());
        responseBodyMap.put("body", null);
        return ServerResponse
                // http返回码
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                // 类型和以前一样
                .contentType(MediaType.APPLICATION_JSON)
                // 响应body的内容
                .body(BodyInserters.fromValue(responseBodyMap));
    }
}

3、注册异常处理器

java 复制代码
@Configuration(proxyBeanMethods = false)
public class ExceptionHandlerConfig {
    private final ServerProperties serverProperties;

    public ExceptionHandlerConfig(ServerProperties serverProperties) {
        this.serverProperties = serverProperties;
    }

    @Bean
    public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes,
                                                             WebProperties webProperties, ObjectProvider<ViewResolver> viewResolvers,
                                                             ServerCodecConfigurer serverCodecConfigurer, ApplicationContext applicationContext) {
        CustomErrorWebExceptionHandler exceptionHandler = new CustomErrorWebExceptionHandler(errorAttributes,
                webProperties.getResources(), this.serverProperties.getError(), applicationContext);
        exceptionHandler.setViewResolvers(viewResolvers.orderedStream().collect(Collectors.toList()));
        exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
        exceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
        return exceptionHandler;
    }
}

4、请求效果

相关推荐
wenyue11211 小时前
Revolutionize Your Kubernetes Experience with Easegress: Kubernetes Gateway API
容器·kubernetes·gateway
木子_lishk2 小时前
gateway 支持同时使用 https 和 http访问
https·gateway
一元咖啡2 小时前
SpringCloud Gateway转发请求到同一个服务的不同端口
spring·spring cloud·gateway
天天扭码3 小时前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
跳跳的向阳花4 小时前
03-03、SpringCloud第三章,负载均衡Ribbon和Feign
spring cloud·ribbon·负载均衡
天天扭码19 小时前
五天SpringCloud计划——DAY1之mybatis-plus的使用
java·spring cloud·mybatis
luckywuxn1 天前
Spring Cloud Alibaba、Spring Cloud 与 Spring Boot各版本的对应关系
spring boot·spring·spring cloud
wclass-zhengge2 天前
SpringCloud篇(服务网关 - GateWay)
spring boot·spring cloud·gateway
荆州克莱2 天前
Redis | Redis常用命令及示例总结(API)
spring boot·spring·spring cloud·css3·技术