Spring 统一异常处理的方式

简介: Spring 统一异常处理的方式

最近帮公司面试的时候,问的最多的问题就是Spring统一异常处理的方式你知道哪几种?这本身并不是一个很难的问题,说实话,会一种方式在工作中完全就可以了。毕竟每种的方式其实都是八九不离十的。

  1:AOP处理

   因为现在Spring Boot的流行,所以很多人第一个想到的都是AOP。这里不做过多的介绍,之前的一篇博客中有说过关于AOP的一些运行机制  Spring AOP @After,@Around,@Before执行的顺序以及可能遇到的问题  。基本就是基于注解的方式,而后通过 @Around 中   catch Exception  或者 @AfterThrowing 等等。

   2:通过过滤器实现

   过滤器实现其实和AOP并没有差距太大,AOP是基于注解的方式,需要匹配到切点以后才可以对方法进行处理,而过滤器的话是基于URL而已,最本质的区别应该就在于此。对于异常的捕获方式,AOP是可以返回JSON格式的,而过滤器需要我们手动设定返回的格式,否则一般返回的格式都是 HTML  

void sendErrorResponse(HttpServletResponse resp, Exception e) throws IOException {
        if (!resp.isCommitted()) {
            logger.warn("process ApiException: {}", e.getMessage());
            resp.setStatus(400);
            resp.setContentType("application/json");
            PrintWriter pw = resp.getWriter();
            //设定返回格式
            JsonUtil.OBJECT_MAPPER.writeValue(pw, ResponseWrapper.fail(e));
            pw.flush();
        } else {
            logger.warn("Cannot send fail response for response is already committed.", e);
        }
    }



3:基于注解 @ControllerAdvice

   这里的基于注解和AOP的基于注解的方式还是有略微的一点不同的地方的。    

   了解新东西最快的方式就是追踪源码了,通过追踪源码可以看到 Spring 在启动的时候会像 BeanFactory 中注册bean。

   ExceptionHandlerExceptionResolver 中会初始化所有的ExceptionHandler  


private void initExceptionHandlerAdviceCache {}



 在执行  findAnnotatedBeans 方法时,会获取当前的 @ControllerAdvice 的一些参数,其中最为重要的我认为应该是获取@Order注解的值了


private static int initOrderFromBeanType(@Nullable Class<?> beanType) {
    Integer order = null;
    if (beanType != null) {
      order = OrderUtils.getOrder(beanType);
    }
    return (order != null ? order : Ordered.LOWEST_PRECEDENCE);
  }


从上述代码可以看到,如果在类上添加了@Order注解,那么我们会获取@Order相对应的值,否则的话就返回他的最低等级


int LOWEST_PRECEDENCE = 2147483647;

PS:  @Order 注解是值越小,越先执行;

在同一个Java服务中,我们可以有多个@ControlerAdvice注解对应的类,@Order在此时有了他的作用,如果两个类中都处理接收了某一种异常的时候,那么会根据Order的加载顺序,谁先加载那么就用谁的异常接收处理;

// 获取到所有的异常处理类
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
// 根据Order 排序,继承了 OrderCompartor
    AnnotationAwareOrderComparator.sort(adviceBeans);
// 排序的方法重写在 OrderCompartor中
 private int doCompare(@Nullable Object o1, @Nullable Object o2, @Nullable OrderComparator.OrderSourceProvider sourceProvider) {
        boolean p1 = o1 instanceof PriorityOrdered;
        boolean p2 = o2 instanceof PriorityOrdered;
        if (p1 && !p2) {
            return -1;
        } else if (p2 && !p1) {
            return 1;
        } else {
            int i1 = this.getOrder(o1, sourceProvider);
            int i2 = this.getOrder(o2, sourceProvider);
            return Integer.compare(i1, i2);
        }
    }

  在接收到异常的时候,Spring也非常智能的说明了,Spring异常的处理方式和你所声明的位置无关,和是否最匹配有关。

  意思就是:我们知道Exception是所有异常的父类,如果我们抛出了一个NullPointerException,在Spring的ControllerAdvice中如果我们定义了 Handler Exception 以及 Handler NullPointerException,那么Spring会在哪一个Handler里面做处理呢?

  答案是会在NullPointerException里面处理  

@ExceptionHandler(Exception.class)
    public ResponseWrapper handleException(Exception e) {
        return returnResult(e, e.getMessage(), ApiError.INTERNAL_SERVER_ERROR);
    }
    @ExceptionHandler(FeignException.class)
    public ResponseWrapper handleFeignException(Exception e) {
        return returnResult(e, e.getMessage(), ApiError.SYSTEM_MAINTAIN);
    }

在代码中,我们分别对Exception 和 FeignException 做了拦截处理,此时我们对服务抛出了一个FeignException异常,代码会在 ExceptionHandlerExceptionResolver 中的  getExceptionHandlerMethod  获取到异常方法以及所报的异常

    @Nullable 
    protected ServletInvocableHandlerMethod getExceptionHandlerMethod(    
    @Nullable HandlerMethod handlerMethod, Exception exception) {}

    PS:如果是第二次仍然是这个方法报这个异常的话,Spring会有自己的缓存机制,会从 exceptionHandlerCacbe中直接获取该方法的处理对象。  

     第一次报此异常时,Spring会遍历  exceptionHandlerAdviceCache,而这个Map就是我们刚才所说的,如果我们有多个 @ControllerAdvice的话,那么Spring 会将其都保存在这个LinkedHashMap之中,并根据Order排序

    for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
          ControllerAdviceBean advice = entry.getKey();
          if (advice.isApplicableToBeanType(handlerType)) {
            ExceptionHandlerMethodResolver resolver = entry.getValue();
            Method method = resolver.resolveMethod(exception);
            if (method != null) {
              return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
            }
          }
        }

    遍历这个bean下的所有已经注册的Handler方法,往下深入发现,在以下代码的时候出现了一个match的数组。

    @Nullable
      private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
        List<Class<? extends Throwable>> matches = new ArrayList<>();
        for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
          if (mappedException.isAssignableFrom(exceptionType)) {
            matches.add(mappedException);
          }
        }
        if (!matches.isEmpty()) {
          matches.sort(new ExceptionDepthComparator(exceptionType));
          return this.mappedMethods.get(matches.get(0));
        }
        else {
          return null;
        }
      }



    这个match数组用来存储我们刚才所说的,如果该 bean 下注册的Handler方法能匹配上当前报错的方法,那么就会加到当前的 match 数组中。

    而后看到,match根据一个排序规则进行排序了,这就是我们所说的内部最优排序规则

    public ExceptionDepthComparator(Class<? extends Throwable> exceptionType) {
            Assert.notNull(exceptionType, "Target exception type must not be null");
            this.targetException = exceptionType;
        }
        public int compare(Class<? extends Throwable> o1, Class<? extends Throwable> o2) {
            int depth1 = this.getDepth(o1, this.targetException, 0);
            int depth2 = this.getDepth(o2, this.targetException, 0);
            return depth1 - depth2;
        }
     private int getDepth(Class<?> declaredException, Class<?> exceptionToMatch, int depth) {
            if (exceptionToMatch.equals(declaredException)) {
                return depth;
            } else {
                return exceptionToMatch == Throwable.class ? 2147483647 : this.getDepth(declaredException, exceptionToMatch.getSuperclass(), depth + 1);
            }
        }




    ControllerAdvice 可以指定多个,如果指定多个会按照最后加载的那个才有效,因此如果有多个 ControllerAdvice,@Order 注解可以加在类上规定执行顺序

    ControllerAdvice 指定多个的情况下,如果有 @ExceptionHandler 相同的,那么会根据 @Order排序,只使用最先加载的那个

    * 不需要指定方法级别顺序,指定了也无效,因为ExceptionHandler是根据最匹配原则。

    根据排序,如果是最匹配顺序,那么返回0,否则是按递归顺序寻找当前匹配的深度,如果是Throwable.class,那么是最不匹配原则,排名在最后,深度为 2147483647

    否则递归查找当前异常的父类,直到找到为止,按照Depth深度获取第一个异常类返回

    相比之下,使用@ControllerAdvice相对于AOP更灵活简单,但是能掌控的代码才是好代码~

    目录
    相关文章
    |
    2月前
    |
    Java 开发者 UED
    Spring Boot的全局异常处理机制
    【2月更文挑战第13天】
    68 0
    |
    4月前
    |
    Java Spring
    【Spring Boot】logback和log4j日志异常处理
    【1月更文挑战第25天】【Spring Boot】logback和log4j日志异常处理
    |
    4月前
    |
    Dubbo Java 应用服务中间件
    微服务框架(十四)Spring Boot @ControllerAdvice异常处理
    此系列文章将会描述Java框架Spring Boot、服务治理框架Dubbo、应用容器引擎Docker,及使用Spring Boot集成Dubbo、Mybatis等开源框架,其中穿插着Spring Boot中日志切面等技术的实现,然后通过gitlab-CI以持续集成为Docker镜像。   本文为Spring Boot使用@ControllerAdvice进行自定义异常捕捉
    |
    7月前
    |
    前端开发 Java API
    Spring MVC异常处理
    Spring MVC异常处理
    35 0
    |
    5月前
    |
    应用服务中间件
    Spring-boot启动失败 Unregistering JMX-exposed beans on shutdown 异常处理
    Spring-boot启动失败 Unregistering JMX-exposed beans on shutdown 异常处理
    68 0
    |
    2月前
    |
    Java 编译器 API
    Spring Boot 异常处理
    Java异常分为 Throwable 类的两个子类:Error 和 Exception。Error 是不可捕获的,由JVM处理并可能导致程序终止,如 OutOfMemoryError。Exception 是可捕获的,包括运行时异常如 ArrayIndexOutOfBoundsException 和编译时异常如 IOException。
    15 1
    |
    2月前
    |
    Java 数据库 开发者
    |
    7月前
    |
    JSON 前端开发 Java
    构建健壮的Spring MVC应用:JSON响应与异常处理
    构建健壮的Spring MVC应用:JSON响应与异常处理
    35 0
    |
    4月前
    |
    前端开发 Java UED
    如何使用 Spring Boot 实现全局异常处理
    如何使用 Spring Boot 实现全局异常处理
    |
    5月前
    |
    前端开发 Java UED
    解密Spring MVC异常处理:从局部到全局,打造稳固系统的关键步骤
    解密Spring MVC异常处理:从局部到全局,打造稳固系统的关键步骤
    72 0