添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
注册/登录

Spring MVC 异常解析器,原理就是这么简单

开发 前端
因为@ExceptionHandler注解的方式已经足够强大,所以我们一般也很少通过实现HandlerExceptionResolver来自定义异常处理策略。

[[330436]]

一般自定义异常处理策略有两种方式

  • 使用@ExceptionHandler注解
  • 实现HandlerExceptionResolver接口
  • 因为@ExceptionHandler注解的方式已经足够强大,所以我们一般也很少通过实现HandlerExceptionResolver来自定义异常处理策略。

    简单介绍一下@ExceptionHandler的使用,后面会结合这些例子进行源码分析

    1. @RestController 
    2. @RequestMapping("location"
    3. public class LocationController { 
    4.  
    5.  @RequestMapping("getLocationInfo"
    6.  public String index() { 
    7.   int sum = 10 / 0; 
    8.   return "locationInfo"
    9.  } 
    10.  
    11.  @ExceptionHandler(RuntimeException.class) 
    12.  public String processRuntimeException() { 
    13.   return "LocationController -> 发生RuntimeException"
    14.  } 
    15.  
    16.  @ExceptionHandler(Exception.class) 
    17.  public String processException() { 
    18.   return "LocationController -> 发生Exception"
    19.  } 

    访问如下链接,返回结果为

    1. http://localhost:8080/location/getLocationInfo 
    2. LocationController -> 发生RuntimeException 

    把processRuntimeException方法注释掉以后,再次访问上面的链接,结果为

    1. LocationController -> 发生Exception 

    如果在每个Controller里面都写异常解析器还是很麻烦的,能不能在一个地方统一处理异常呢?当然可以,这时候就不得不用到@RestControllerAdvice或者@ControllerAdvice

    写如下的全局异常解析器

    1. @RestControllerAdvice 
    2. public class MyExceptionHandler { 
    3.  
    4.  @ExceptionHandler(RuntimeException.class) 
    5.  public String processRuntimeException() { 
    6.   return "MyExceptionHandler -> 发生RuntimeException"
    7.  } 
    8.  
    9.  @ExceptionHandler(Exception.class) 
    10.  public String processException() { 
    11.   return "MyExceptionHandler -> 发生RuntimeException"
    12.  } 

    访问上面的链接,返回结果为

    1. LocationController -> 发生Exception 

    我们把LocationController类的processException方法也注释掉,此时LocationController类里面已经没有被@ExceptionHandler注解标记的方法了

    访问上面的链接,返回结果为

    1. MyExceptionHandler -> 发生RuntimeException 

    把MyExceptionHandler中的processRuntimeException方法注释掉访问上面的链接,返回结果为

    1. MyExceptionHandler -> 发生Exception 

    通过以上的例子,我们可以得出如下结论

  • @RestControllerAdvice或者@ControllerAdvice类内的解析器的优先级低于@RequestMapping类的解析器的优先级
  • 如果一个异常能被多个解析器所处理,则选择继承关系最近的解析器
  • 假设BizException继承自NullPointException A方法解析BizException B方法解析NullPointException C方法解析Exception

    BizException会被A方法解析 NullPointException会被B方法解析 如果没有A方法,则BizException会被B方法解析,如果B方法也没有,则被C方法解析,不难理解哈

    @RestControllerAdvice和@ControllerAdvice有什么区别呢?

    名字上就可以猜出@RestControllerAdvice只是在@ControllerAdvice的基础上加了@ResponseBody注解,看一波源码也确实如此。所以@RestControllerAdvice类最终返回的是JSON,@ControllerAdvice最终返回的是视图。如果你不明白为什么加了@ResponseBody注解最终返回的内容为JSON,建议看一下返回值处理器相关的内容

    异常解析器接口定义如下

    1. public interface HandlerExceptionResolver { 
    2.  
    3.  // 将异常封装为ModelAndView后返回 
    4.  @Nullable 
    5.  ModelAndView resolveException( 
    6.    HttpServletRequest request, HttpServletResponse response,  
    7.    @Nullable Object handler, Exception ex); 
    8.  

    Spring MVC默认的异常解析器存放在如下属性中

    1. @Nullable 
    2. private List<HandlerExceptionResolver> handlerExceptionResolvers; 

    顺序依次为

  • ExceptionHandlerExceptionResolver
  • ResponseStatusExceptionResolver
  • DefaultHandlerExceptionResolver
  • UML图如下

    Order接口是用来排序的哈,Spring MVC默认的解析器不是通过Order接口来控制顺序的,因为默认的解析器都继承自AbstractHandlerExceptionResolver,并且都没有重写getOrder方法

    对Spring MVC比较清楚的小伙伴应该都知道DispatcherServlet属性的默认实现都定义在源码包的DispatcherServlet.properties文件中,List的顺序也是按这个来的。放一部分内容

    1. org.springframework.web.servlet.HandlerAdapter= 
    2.     org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\ 
    3.  org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\ 
    4.  org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter 
    5.  
    6. org.springframework.web.servlet.HandlerExceptionResolver= 
    7.     org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\ 
    8.  org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\ 
    9.  org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionReso 

    接下来分析这3个默认的HandlerExceptionResolver

    ExceptionHandlerExceptionResolver

    ExceptionHandlerExceptionResolver用于支持@ExceptionHandler,而@ExceptionHandler应该是我们最常的,方便我们自定义异常处理策略,比通过实现HandlerExceptionResolver接口的方式简单

    从AbstractHandlerMethodExceptionResolver#shouldApplyTo可以看到

    1. @Override 
    2. protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) { 
    3.  if (handler == null) { 
    4.   // handler为空,交给父类去判断 
    5.   // 默认该逻辑返回true 
    6.   return super.shouldApplyTo(request, null); 
    7.  } 
    8.  else if (handler instanceof HandlerMethod) { 
    9.   HandlerMethod handlerMethod = (HandlerMethod) handler; 
    10.   handler = handlerMethod.getBean(); 
    11.   // 交给父类判断 
    12.   return super.shouldApplyTo(request, handler); 
    13.  } 
    14.  else { 
    15.   // 不支持 
    16.   return false
    17.  } 

    只有当handler为空或者handler的类型为HandlerMethod时(@RequestMapping返回的类型为HandlerMethod)才会执行后面的异常解析逻辑。所以你通过实现Controller接口或者实现HttpRequestHandler接口定义的Handler,这个注解是不起作用的

    @ExceptionHandler的处理过程主要和下面2个类有关系ExceptionHandlerExceptionResolver,ExceptionHandlerMethodResolver

    用几个成员变量说一下处理过程,就不贴过多的代码了

    ExceptionHandlerExceptionResolver

    1. // 省略了继承和实现关系 
    2. public class ExceptionHandlerExceptionResolver { 
    3.  
    4.  @Nullable 
    5.  private HandlerMethodArgumentResolverComposite argumentResolvers; 
    6.  
    7.  @Nullable 
    8.  private HandlerMethodReturnValueHandlerComposite returnValueHandlers; 
    9.  
    10.  private List<HttpMessageConverter<?>> messageConverters; 
    11.  
    12.  
    13.  // 被@RequestMapping标记的类 -> ExceptionHandlerMethodResolver 
    14.  private final Map<Class<?>, ExceptionHandlerMethodResolver>  
    15.  exceptionHandlerCache = new ConcurrentHashMap<>(64); 
    16.  
    17.  // 被@ControllerAdvice注解标记的类 -> ExceptionHandlerMethodResolver 
    18.  private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> 
    19.  exceptionHandlerAdviceCache = new LinkedHashMap<>(); 

    可以看到ExceptionHandlerExceptionResolver类定义了自己的参数处理器,返回值处理器,消息转换器。所以你可以通过这些组件反向知道@ExceptionHandler方法支持的参数类型

    例如从如下方法可以知道,支持的参数类型为@SessionAttribute,@RequestAttribute等 如果你写个@RequestParam是肯定不会注入进来的

    1. protected List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() { 
    2.  List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(); 
    3.  
    4.  // Annotation-based argument resolution 
    5.  resolvers.add(new SessionAttributeMethodArgumentResolver()); 
    6.  resolvers.add(new RequestAttributeMethodArgumentResolver()); 
    7.  
    8.  // Type-based argument resolution 
    9.  resolvers.add(new ServletRequestMethodArgumentResolver()); 
    10.  resolvers.add(new ServletResponseMethodArgumentResolver()); 
    11.  resolvers.add(new RedirectAttributesMethodArgumentResolver()); 
    12.  resolvers.add(new ModelMethodProcessor()); 
    13.  
    14.  // Custom arguments 
    15.  if (getCustomArgumentResolvers() != null) { 
    16.   resolvers.addAll(getCustomArgumentResolvers()); 
    17.  } 
    18.  
    19.  return resolvers; 

    最重要的4个map来了,ExceptionHandlerExceptionResolver的工作过程主要就是操作这4个map

    1. // 省略了继承和实现关系 
    2. public class ExceptionHandlerExceptionResolver { 
    3.  
    4.  // 被@RequestMapping标记的类 -> ExceptionHandlerMethodResolver 
    5.  private final Map<Class<?>, ExceptionHandlerMethodResolver> 
    6.  exceptionHandlerCache = new ConcurrentHashMap<>(64); 
    7.  
    8.  // 被@ControllerAdvice注解标记的类 -> ExceptionHandlerMethodResolver 
    9.  private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> 
    10.  exceptionHandlerAdviceCache = new LinkedHashMap<>(); 
    11.     

    exceptionHandlerCache保存了@RequestMapping对应的ExceptionHandlerMethodResolver,是在执行异常解析的过程中被赋值的

    exceptionHandlerAdviceCache保存了@ControllerAdvice对应的 ExceptionHandlerMethodResolver,是在ExceptionHandlerExceptionResolver被初始化的过程中赋值的

    而ExceptionHandlerMethodResolver你可以认为只是封装了一下Exception及其对应的Method

    以最开始的例子演示,ExceptionHandlerExceptionResolver初始化后

    此时exceptionHandlerCache是没有值的 访问如下链接后

    1. http://localhost:8080/location/getLocationInfo 

    exceptionHandlerCache中的值如下,LocationController及其对应的ExceptionHandlerMethodResolver被放了进来追一下以下方法的执行 ExceptionHandlerExceptionResolver#doResolveHandlerMethodException ExceptionHandlerExceptionResolver#getExceptionHandlerMethod

    可以得出我们测试的结论@RestControllerAdvice或者@ControllerAdvice类内的解析器的优先级低于@RequestMapping类的解析器的优先级

    总体实现也不难,从exceptionHandlerCache中能找到解析器就返回执行,找不到就从exceptionHandlerAdviceCache中找,这不是就实现了优先级了吗?

    接着来看剩下的2个Map

    1. public class ExceptionHandlerMethodResolver { 
    2.  
    3.  
    4.  // 异常 -> 对应的处理方法 
    5.  private final Map<Class<? extends Throwable>, Method> 
    6.  mappedMethods = new HashMap<>(16); 
    7.  
    8.  // 异常 -> 对应的处理方法 
    9.  // 这个是基于mappedMethods又做了一次缓存 
    10.  // 为什么要再做一次缓存呢? 
    11.  // 是因为根据异常类型获取处理方法的时候,一个异常可能有多个处理方法,即一个异常会从mappedMethods中查出多个处理方法 
    12.  // 最后返回的是继承关系最近的异常对应的处理方法,所以在查找的时候又做了一次缓存,避免每次查mappedMethods然后取最优值 
    13.  // 从exceptionLookupCache中就可以直接查到最优的处理方法 
    14.  private final Map<Class<? extends Throwable>, Method> 
    15.  exceptionLookupCache = new ConcurrentReferenceHashMap<>(16); 
    16.   

    @ControllerAdvice的mappedMethods是在ExceptionHandlerExceptionResolver初始化的过程中赋值的

    @RequestMapping的mappedMethods是在执行异常解析的过程中被赋值的

    而exceptionLookupCache是在异常解析过程中,通过Exception查找Method的过程中基于mappedMethods做的缓存

    为什么在查找过程中要再做一次缓存呢?

    是因为根据异常类型获取处理方法的时候,一个异常可能有多个处理方法,即一个异常会从mappedMethods中查出多个处理方法,最后返回的是继承关系最近的异常对应的处理方法,所以在查找的时候又做了一次缓存,避免每次查mappedMethods然后取最优值。从exceptionLookupCache中就可以直接查到最优的处理方法

    以LocationController为例,查找一次异常后,exceptionLookupCache的值如下

    这样当再次发生ArithmeticException异常时就能从exceptionLookupCache找到对应的处理方法

    ResponseStatusExceptionResolver

    ResponseStatusExceptionResolver和DefaultHandlerExceptionResolver的实现都不是很难,就不进行过多的分析了

    ResponseStatusExceptionResolver主要用来处理如下异常

    抛出的异常类型继承自ResponseStatusException

    抛出的异常类型被@ResponseStatus标记

    以一个例子来演示这个处理器的功能

    1. @ResponseStatus(HttpStatus.UNAUTHORIZED) 
    2. public class UnauthorizedException extends RuntimeException { 
    3. @RestController 
    4. @RequestMapping("shoppingCar"
    5. public class ShoppingCarController { 
    6.  
    7.  @RequestMapping("getCarInfo"
    8.  public String index() { 
    9.   throw new UnauthorizedException(); 
    10.  } 
    1. http://localhost:8080/shoppingCar/getCarInfo 

    DefaultHandlerExceptionResolver

    用来处理一些常见的Http异常,如

    400:请求无效 405:请求方法不支持 500:内部服务器错误

    1. # DispatcherServlet#processDispatchResult的部分方法 
    2. // 处理过程发生了异常 
    3. if (exception != null) { 
    4.  if (exception instanceof ModelAndViewDefiningException) { 
    5.   logger.debug("ModelAndViewDefiningException encountered", exception); 
    6.   // 直接使用异常中封装的ModelAndView作为最终的ModelAndView结果 
    7.   mv = ((ModelAndViewDefiningException) exception).getModelAndView(); 
    8.  } 
    9.  else { 
    10.   // 其他异常类型,先获取解析器 
    11.   Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); 
    12.   // 通过异常解析器将异常解析为一个错误视图 
    13.   mv = processHandlerException(request, response, handler, exception); 
    14.   errorView = (mv != null); 
    15.  } 

    如果整个处理过程发生异常,依次调用DispatcherServlet的成员变量handlerExceptionResolvers的resolveException方法,找到第一个不为null的ModelAndView,然后返回

    1. @Nullable 
    2. private List<HandlerExceptionResolver> handlerExceptionResolvers; 

    本文转载自微信公众号「Java识堂」,可以通过以下二维码关注。转载本文请联系Java识堂公众号。

    责任编辑:武晓燕 Java识堂
    点赞
    收藏