2023-02-27  阅读(55)
原文作者:Java日知录 原文地址:https://javadaily.cn

前言

在单体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解析异常

202302272301543472.png

jwt解析异常

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

    {
      "timestamp": "2020-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"
    }
  • 服务下线

202302272301550103.png

服务下线异常

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

    {
      "timestamp": "2020-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]);
                }
            }));
        }
    }

测试结果

202302272301556734.png

测试结果

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


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文