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