前言
阅读上文,了解到了可以通过自定义HandlerExceptionResolver实现来处理程序异常,当然Spring MVC也内置了一些实现来对异常处理进行支持。但是作为新时代的程序员,我估计已经很少人知道HandlerExceptionResolver这个异常处理器接口(更有甚者连ModelAndView都没听说过也大有人在啊),虽然这不应该,但存在即合理。因此从现象上可以认为使用自定义HandlerExceptionResolver实现的方式去处理异常已经out了,它已经被新的方式所取代:@ExceptionHandler方式,这就是本章节的核心议题,来探讨它的使用以及原理。
回忆上篇文章讲述HandlerExceptionResolver,你是否疑问过这个问题:通过HandlerExceptionResolver如何返回一个json串呢?其实这个问题雷同于:源生Servlet如何给前端返回一个json串呢?因为上文的示例都是返回的一个ModelAndView页面,so本文在最开头先解决这个疑问,为下面内容做个铺垫吧。
HandlerExceptionResolver如何返回JSON格式数据?
基于上篇文章案例自定义了一个异常处理器来处理Handler抛出的异常,示例中返回的是一个页面ModelAndView。但是通常情况下我们的应用都是REST应用,我们的接口返回的都是一个JSON串,那么若接口抛出异常的话我们处理好后也同样的返回一个JSON串比返回一个页面更为合适。
这时若你项目较老,使用的仍旧是HandlerExceptionResolver方式处理异常的话,我在本处提供两种处理方式,供以参考:
方式一:response直接输出json
自定义异常处理器(匿名实现):
@Configuration @EnableWebMvc public class WebMvcConfig extends WebMvcConfigurerAdapter { @Override public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) { // 自定义异常处理器一般请放在首位 exceptionResolvers.add(0, new AbstractHandlerExceptionResolver() { @Override protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { response.setContentType("application/json;charset=UTF-8"); response.setCharacterEncoding("UTF-8"); try { String jsonStr = ""; if (ex instanceof BusinessException) { response.setStatus(HttpStatus.OK.value()); jsonStr = "{'code':100001,'message':'业务异常,请联系客服处理'}"; } else { response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); jsonStr = "{'code':500,'message':'服务器未知异常'}"; } response.getWriter().print(jsonStr); response.getWriter().flush(); response.getWriter().close(); } catch (IOException e) { e.printStackTrace(); } return null; } }); } }
访问截图如下:
注意事项:
- 因为return null,所以后面若还有处理器将继续执行。但因为本处已把response close了,因此请确保后面不会再使用此response
- 若所有Resolver处理完后还是return null,那Spring MVC将直接throw ex,因此你看到的效果是:控制台上有异常栈,但是前段页面上显示是友好的json串。
- 因为木有ModelAndView(值为null),所以不会有渲染步骤,因此后续步骤Spring MVC也不会再使用到response(自定义的拦截器除外~)。
方式二:借助MappingJackson2JsonView
自定义异常处理器,借助MappingJackson2JsonView这个json视图实现:
@Configuration @EnableWebMvc public class WebMvcConfig extends WebMvcConfigurerAdapter { @Override public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) { // 自定义异常处理器一般请放在首位 exceptionResolvers.add(0, new AbstractHandlerExceptionResolver() { @Override protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { ModelAndView mv = new ModelAndView(); MappingJackson2JsonView view = new MappingJackson2JsonView(); view.setJsonPrefix("fsxJson"); // 设置JSON前缀,有的时候很好用的哦 //view.setModelKey(); // 让只序列化指定的key mv.setView(view); // 这样添加key value就非常方便 mv.addObject("code", "100001"); mv.addObject("message", "业务异常,请联系客服处理"); return mv; } }); } }
访问截图如下:
显然这种使用JsonView的方式代码看起来更加舒服,使用起来更加的面向对象。
这两种方式都是基于自定义HandlerExceptionResolver实现类的方式来处理异常,最终给前端返回一个json串。虽然方式二看起来步骤也不麻烦,也够面向对象,但接下来的@ExceptionHandler方式可谓是杀手级的应用~
@ExceptionHandler
此注解是Spring 3.0后提供的处理异常的注解,整个Spring在3.0+中新增了大量的能力来对REST应用提供支持,此注解便是其中之一。
它(只能)标注在方法上,可以使得这个方法成为一个异常处理器,处理指定的异常类型。
// @since 3.0 @Target(ElementType.METHOD) // 只能标注在方法上 @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ExceptionHandler { // 指定异常类型,可以多个 Class<? extends Throwable>[] value() default {}; }
上篇讲解HandlerExceptionResolver的原理部分讲到了,DispatcherServlet对异常的处理最终都是无一例外的交给了HandlerExceptionResolver异常处理器,因此很容易想到@ExceptionHandler它的底层实现原理其实也是一个异常处理器,它便是:ExceptionHandlerExceptionResolver。
在分析它之前,需要先前置介绍两个类:AbstractHandlerMethodExceptionResolver和ExceptionHandlerMethodResolver
AbstractHandlerMethodExceptionResolver
它是ExceptionHandlerExceptionResolver的抽象父类,服务于处理器类型是HandlerMethod类型的抛出的异常,它并不规定实现方式必须是@ExceptionHandler。它复写了抽象父类AbstractHandlerExceptionResolver的shouldApplyTo方法:
// @since 3.1 专门处理HandlerMethod类型是HandlerMethod类型的异常 public abstract class AbstractHandlerMethodExceptionResolver extends AbstractHandlerExceptionResolver { // 只处理HandlerMethod这种类型的处理器抛出的异常~~~~~~ @Override protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) { if (handler == null) { return super.shouldApplyTo(request, null); } else if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; // 可以看到最终getBean表示最终哪去验证的是它所在的Bean类,而不是方法本身 // 所以异常的控制是针对于Controller这个类的~ handler = handlerMethod.getBean(); return super.shouldApplyTo(request, handler); } else { return false; } } @Override @Nullable protected final ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) { return doResolveHandlerMethodException(request, response, (HandlerMethod) handler, ex); } @Nullable protected abstract ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception ex); }
此抽象类非常简单:规定了只处理HandlerMethod抛出的异常。
ExceptionHandlerMethodResolver(重要)
它是一个会在Class及Class的父类中找出带有@ExceptionHandler注解的类,该类带有key为Throwable,value为Method的缓存属性,提供匹配效率。
// @since 3.1 public class ExceptionHandlerMethodResolver { // A filter for selecting {@code @ExceptionHandler} methods. public static final MethodFilter EXCEPTION_HANDLER_METHODS = method -> AnnotatedElementUtils.hasAnnotation(method, ExceptionHandler.class); // 两个缓存:key:异常类型 value:目标方法Method private final Map<Class<? extends Throwable>, Method> mappedMethods = new HashMap<>(16); private final Map<Class<? extends Throwable>, Method> exceptionLookupCache = new ConcurrentReferenceHashMap<>(16); // 唯一构造函数 // detectExceptionMappings:传入method,找到这个Method可以处理的所有的异常类型们(注意此方法的逻辑) // addExceptionMapping:把异常类型和Method缓存进mappedMethods里 public ExceptionHandlerMethodResolver(Class<?> handlerType) { for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) { for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) { addExceptionMapping(exceptionType, method); } } } // 找到此Method能够处理的所有的异常类型 // 1、detectAnnotationExceptionMappings:本方法或者父类的方法上标注有ExceptionHandler注解,然后读取出其value值就是它能处理的异常们 // 2、若value值木有指定,那所有的方法入参们的异常类型,就是此方法能够处理的所有异常们 // 3、若最终还是空,那就抛出异常:No exception types mapped to " + method private List<Class<? extends Throwable>> detectExceptionMappings(Method method) { List<Class<? extends Throwable>> result = new ArrayList<>(); detectAnnotationExceptionMappings(method, result); if (result.isEmpty()) { for (Class<?> paramType : method.getParameterTypes()) { if (Throwable.class.isAssignableFrom(paramType)) { result.add((Class<? extends Throwable>) paramType); } } } if (result.isEmpty()) { throw new IllegalStateException("No exception types mapped to " + method); } return result; } // 对于添加方法一样有一句值得说的: // 若不同的Method表示可以处理同一个异常,那是不行的:"Ambiguous @ExceptionHandler method mapped for [" // 注意:此处必须是同一个异常(比如Exception和RuntimeException不属于同一个...) private void addExceptionMapping(Class<? extends Throwable> exceptionType, Method method) { Method oldMethod = this.mappedMethods.put(exceptionType, method); if (oldMethod != null && !oldMethod.equals(method)) { throw new IllegalStateException("Ambiguous @ExceptionHandler method mapped for [" + exceptionType + "]: {" + oldMethod + ", " + method + "}"); } } // 给指定的异常exception匹配上一个Method方法来处理 // 若有多个匹配上的:使用ExceptionDepthComparator它来排序。若木有匹配的就返回null @Nullable public Method resolveMethod(Exception exception) { return resolveMethodByThrowable(exception); } // @since 5.0 递归到了couse异常类型 也会处理 @Nullable public Method resolveMethodByThrowable(Throwable exception) { Method method = resolveMethodByExceptionType(exception.getClass()); if (method == null) { Throwable cause = exception.getCause(); if (cause != null) { method = resolveMethodByExceptionType(cause.getClass()); } } return method; } //1、先去exceptionLookupCache找,若匹配上了直接返回 // 2、再去mappedMethods这个缓存里找。很显然可能匹配上多个,那就用ExceptionDepthComparator排序匹配到一个最为合适的 // 3、匹配上后放进缓存`exceptionLookupCache`,所以下次进来就不需要再次匹配了,这就是缓存的效果 // ExceptionDepthComparator的基本理论上:精确匹配优先(按照深度比较) @Nullable public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) { Method method = this.exceptionLookupCache.get(exceptionType); if (method == null) { method = getMappedMethod(exceptionType); this.exceptionLookupCache.put(exceptionType, method); } return method; } }
对于本类的功能,可总结如下:
1.找到指定Class类(可能是Controller本身,也可能是@ControllerAdvice)里面所有标注有@ExceptionHandler的方法们
2.同一个Class内,不能出现同一个(注意理解同一个的含义)异常类型被多个Method处理的情况,否则抛出异常:Ambiguous @ExceptionHandler method mapped for ...
1. 相同异常类型处在不同的Class内的方法上是可以的,比如常见的一个在Controller内,一个在@ControllerAdvice内~
3.提供缓存:
1. mappedMethods:每种异常对应的处理方法(直接映射代码上书写的异常-方法映射)
2. exceptionLookupCache:经过按照深度逻辑精确匹配上的Method方法
4.既能处理本身的异常,也能够处理getCause()导致的异常
5.ExceptionDepthComparator的匹配逻辑是按照深度匹配。比如发生的是NullPointerException,但是声明的异常有Throwable和Exception,这是它会根据异常的最近继承关系找到继承深度最浅的那个异常,即Exception。