Spring Cloud Alibaba微服务实战二十四 - SpringCloud Gateway的全局异常处理

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

学习必须往深处挖,挖的越深,基础越扎实!

阶段1、深入多线程

阶段2、深入多线程设计模式

阶段3、深入juc源码解析


阶段4、深入jdk其余源码解析


阶段5、深入jvm源码解析

码哥源码部分

码哥讲源码-原理源码篇【2024年最新大厂关于线程池使用的场景题】

码哥讲源码【炸雷啦!炸雷啦!黄光头他终于跑路啦!】

码哥讲源码-【jvm课程前置知识及c/c++调试环境搭建】

​​​​​​码哥讲源码-原理源码篇【揭秘join方法的唤醒本质上决定于jvm的底层析构函数】

码哥源码-原理源码篇【Doug Lea为什么要将成员变量赋值给局部变量后再操作?】

码哥讲源码【你水不是你的错,但是你胡说八道就是你不对了!】

码哥讲源码【谁再说Spring不支持多线程事务,你给我抽他!】

终结B站没人能讲清楚红黑树的历史,不服等你来踢馆!

打脸系列【020-3小时讲解MESI协议和volatile之间的关系,那些将x86下的验证结果当作最终结果的水货们请闭嘴】

前言

在单体SpringBoot项目中我们需要捕获全局异常只需要在项目中配置 @RestControllerAdvice和 @ExceptionHandler就可以针对不同类型异常进行统一处理,统一包装后返回给前端调用方。

    @Slf4j
    @RestControllerAdvice
    public class RestExceptionHandler {
        /**
         * 默认全局异常处理。
         * @return ResultData
         */
        @ExceptionHandler(Exception.class)
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
        public ResultData<String> exception(Exception e) {
            log.error("全局异常信息 ex={}", e.getMessage(), e);
            return ResultData.fail(ReturnCode.RC500.getCode(),e.getMessage());
        }
    }

但是在微服务架构下,例如网关调用业务系统失败(比如网关层jwt token解析异常、服务下线)这时候应用层的 @RestControllerAdvice就会不生效,因为此时流量根本没到应用层。

下面我们分别模拟两种场景,让大家感受一下:

  • jwt解析异常

jwt解析异常

故意写错token让其无法解析,后端返回的数据为:

    {
      "timestamp": "2023-12-22T02:32:03.143+0000",
      "path": "/account-service/account/test/jianzh5",
      "status": 500,
      "error": "Internal Server Error",
      "message": "Cannot convert access token to JSON",
      "requestId": "7043b1f8-1"
    }
  • 服务下线

服务下线异常

停止后端服务,后端返回的数据为:

    {
      "timestamp": "2023-12-22T02:36:13.281+0000",
      "path": "/account-service/account/getByCode/jianzh5",
      "status": 503,
      "error": "Service Unavailable",
      "message": "Unable to find instance for account-service",
      "requestId": "7043b1f8-6"
    }

在前后端分离的项目中,一般都要约定项目整体返回格式,前端需要根据返回数据确定页面逻辑。在我们项目例子中我们约定好的响应格式如下:

    @Data
    @ApiModel(value = "统一返回结果封装",description = "接口返回统一结果")
    public class ResultData<T> {
        /** 结果状态 ,具体状态码参见ResultData.java*/
        @ApiModelProperty(value = "状态码")
        private int status;
        @ApiModelProperty(value = "响应信息")
        private String message;
        @ApiModelProperty(value = "后端返回结果")
        private T data;
        @ApiModelProperty(value = "后端响应状态")
        private boolean success;
        @ApiModelProperty(value = "响应时间戳")
        private long timestamp ;
    
        public ResultData (){
            this.timestamp = System.currentTimeMillis();
        }
     ...
    }

很显然在这些情况下返回的异常数据并不符合我们的预期格式,我们需要改造网关返回数据。

原因剖析

在SpringCloud gateway中默认使用 DefaultErrorWebExceptionHandler来处理异常。这个可以通过配置类 ErrorWebFluxAutoConfiguration得之。

在 DefaultErrorWebExceptionHandler类中的默认异常处理逻辑如下:

    public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
     ...
        protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
            return RouterFunctions.route(this.acceptsTextHtml(), this::renderErrorView).andRoute(RequestPredicates.all(), this::renderErrorResponse);
        }
       ...
    }

根据请求头确认返回什么资源格式。

返回的数据内容在 DefaultErrorAttributes类中构建而成。

    public class DefaultErrorAttributes implements ErrorAttributes {
     ...
        public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
            Map<String, Object> errorAttributes = new LinkedHashMap();
            errorAttributes.put("timestamp", new Date());
            errorAttributes.put("path", request.path());
            Throwable error = this.getError(request);
            MergedAnnotation<ResponseStatus> responseStatusAnnotation = MergedAnnotations.from(error.getClass(), SearchStrategy.TYPE_HIERARCHY).get(ResponseStatus.class);
            HttpStatus errorStatus = this.determineHttpStatus(error, responseStatusAnnotation);
            errorAttributes.put("status", errorStatus.value());
            errorAttributes.put("error", errorStatus.getReasonPhrase());
            errorAttributes.put("message", this.determineMessage(error, responseStatusAnnotation));
            errorAttributes.put("requestId", request.exchange().getRequest().getId());
            this.handleException(errorAttributes, this.determineException(error), includeStackTrace);
            return errorAttributes;
        }
     ...
    }

阅读到这里就可以看到为什么上面会返回那样的数据格式,接下来我们需要改写返回格式。

解决方案

这里我们我们可以自定义一个 CustomErrorWebExceptionHandler类用来继承 DefaultErrorWebExceptionHandler,然后修改生成前端响应数据的逻辑。再然后定义一个配置类,写法可以参考 ErrorWebFluxAutoConfiguration,简单将异常类替换成 CustomErrorWebExceptionHandler类即可。

这种方法大家请自行研究,基本都是复制代码,改写不复杂,这种方法我们就不演示了,这里给大家介绍另外一种写法:

我们定义一个全局异常类 GlobalErrorWebExceptionHandler让其直接实现顶级接口 ErrorWebExceptionHandler重写 handler()方法,在 handler()方法中返回我们自定义的响应类。但是需要注意重写的实现类优先级一定要小于内置 ResponseStatusExceptionHandler 经过它处理的获取对应错误类的响应码。

代码如下:

    /**
     * 网关全局异常处理
     * @author javadaily
     */
    @Slf4j
    @Order(-1)
    @Configuration
    @RequiredArgsConstructor(onConstructor = @__(@Autowired))
    public class GlobalErrorWebExceptionHandler implements ErrorWebExceptionHandler {
    
        private final ObjectMapper objectMapper;
    
        @Override
        public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
            ServerHttpResponse response = exchange.getResponse();
            if (response.isCommitted()) {
                return Mono.error(ex);
            }
    
            // 设置返回JSON
            response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
            if (ex instanceof ResponseStatusException) {
                response.setStatusCode(((ResponseStatusException) ex).getStatus());
            }
    
            return response.writeWith(Mono.fromSupplier(() -> {
                DataBufferFactory bufferFactory = response.bufferFactory();
                try {
                    //返回响应结果
                    return bufferFactory.wrap(objectMapper.writeValueAsBytes(ResultData.fail(500,ex.getMessage())));
                }
                catch (JsonProcessingException e) {
                    log.error("Error writing response", ex);
                    return bufferFactory.wrap(new byte[0]);
                }
            }));
        }
    }

测试结果

测试结果

符合我们的预期结果,实现网关层的异常拦截!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值