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