最近帮公司面试的时候,问的最多的问题就是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更灵活简单,但是能掌控的代码才是好代码~


更多相关文章

  1. IDEA最新激活码2021(IDEA2020.3永久激活方法)
  2. 模板方法模式在开源代码中应用
  3. @Transactional 注解哪些情况下会失效?
  4. 使用 @ControllerAdvice 注解,优化异常处理
  5. 构造方法的参数太多,如何解决?

随机推荐

  1. Android文件存储
  2. Android 结束进程的方法
  3. openGL 简单demo
  4. Android 返回键退出
  5. Android的NDK开发(2)————利用Android
  6. 解决activity加上Theme.Translucent.NoTi
  7. android 广播 android Service 开机启动
  8. RecyclerView 滚动条的显示与隐藏
  9. Android下雪动画 VS JS下雪动画
  10. Android 设置虚线分割线