SpringMVC的组件之HandlerExceptionResolver详解
作者:Evan_L
前言
在介绍完Handler、HandlerAdapter、HandlerMapping之后,剩下的比较关键的组件就是HandlerExceptionResolver、ViewResolver。
其他的像国际化、主题、文件上传、重定向,这些锦上添花的组件都是一个框架需要关心的。
但不是我们平常使用的核心功能,所以有兴趣的同学就自己了解吧。
HandlerExceptionResolver
不管是在处理请求映射(HandlerMapping),还是在请求被处理(Handler)时抛出的异常,DispatcherServlet都会委托给HandlerExceptionResolver进行异常处理。
该接口只有一个方法。
/** * 尝试处理handler执行过程中抛出的异常。可能返回一个代表特定页面的ModelAndView * 如果返回的ModelAndView为空:{ModelAndView#isEmpty()},则表示该异常已经被成功处理,并且不需要渲染视图。例如:通过设置httpStatus处理异常. * @param request 当前HTTP请求 * @param response 当前HTTP响应 * @param handler 被执行的handler。可能为null,如果异常发生在处理器选择之前(例如:multipart处理失败) * @param ex 处理过程中抛出的异常 * @return 对应的ModelAndView。如果无法处理则返回null,DispatcherServlet将使用默认的处理流程。返回new ModelAndView(),则说明请求直接被处理完成了,不需要试图处理。 */ @Nullable ModelAndView resolveException( HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
SpringMVC提供的异常处理器
HandlerExceptionResolver | Description |
SimpleMappingExceptionResolver | 将异常Class简单的映射到错误视图的名字。常用于浏览器应用渲染错误页面 |
DefaultHandlerExceptionResolver | 负责处理SpringMVC抛出的异常,并且将这些异常映射到对应的HTTP状态码。可以对照他的替代者:ResponseEntiryExceptionResolver、Controller Advice |
ResponseStatusExceptionResolver | 通过@ResponseStatus注解来处理异常,并基于注解的值映射到HTTP状态码 |
ExceptionHandlerExceptionResolver | .通过调用@Controller或者@ControllerAdvice中的@ExceptionHandler注解方法来处理异常 |
接下来,我们介绍一下比较常用的ExceptionHandlerExceptionResolver。
ExceptionHandlerExceptionResolver
如果有同学配置过全局异常处理的,应该会认识这两注解:@ControllerAdvice @ExceptionHandler,而他们正是ExceptionHandlerExceptionResolver处理异常的重要抓手。
全局异常配置
在了解ExceptionHandlerExceptionResolver的设计之前,我们先来看看最常用的全局异常配置是怎样的。只有知道他要达到什么样的目标,才能理解他为什么这么设计/实现!
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(RuntimeException.class) @ResponseBody public ResultDTO handleRuntimeException(RuntimeException e, HandlerMethod handlerMethod) { // ... } /** * 这是官方的例子 */ @ExceptionHandler({FileSystemException.class, RemoteException.class}) public ResponseEntity<String> handle(IOException ex) { // ... } }
好,现在来分析一下需求:
- 识别带@ControllerAdvice注解的bean
- 识别这些bean中的@ExceptionHandler方法
- 根据@ExceptionHandler的条件,找到匹配的异常处理方法
- 识别和处理异常处理方法的参数,并准备参数列表 PS: SpringMVC提供了丰富的可选参数
- 识别和处理方法返回值,并通过response给客户端进行响应。
额,有没有觉得似曾相识?对标一下@Controller、@RequestMapping?
Handler领域 | Exception领域 | 作用/描述 |
@Controller | @ControllerAdvice | 标记目标处理对象类 |
@RestController | @RestControllerAdvice | 标记目标处理对象,并表示返回值即为要响应的消息体,通常会被json序列化 |
@RequestMapping | @ExceptionHandler | 标记目标处理方法,并且包含方法可以处理的匹配条件 |
方法参数列表灵活多变,需要进行参数解析 | 相较于HandlerAdapter,支持的参数要少一些。例如:不支持@RequestBody参数 | 方法参数 |
方法返回值灵活多变, 需要进行返回值处理 | 相较于HandlerAdapter,支持的返回值要少一些。例如不支持ModelAndView,但支持ViewName,不支持异步响应返回值等等 | 方法返回值 |
是不是高度相似?这也意味着他们在参数解析和返回值处理上高度相似。
异常处理逻辑
与上面的全局异常处理相比,实际上Spring在处理异常时,还需要考虑@Controller中的@ExceptionHandler方法。
因此异常的处理分为两部分
处理方法 | 描述 |
@ControllerAdvice中的@ExceptionHandler | 这里面的方法是全局性的,所有的@RequestMapping方法只要发生异常且Controller中没有声明异常处理方法,则都会用这些方法处理 |
@Controller中的@ExceptionHandler | 这些异常处理方法则只对该Controller有效。即,只能处理该Controller中的@RequestMapping方法异常 |
由此,我们将不得不有个优先级,同样也是最靠近@RequestMapping优先。只有当对应的@Controller中没有@ExceptionHandler时,才能用全局异常处理方法进行处理。 但是仔细考虑一下,在应用运行过程中,每个类都是固定的,方法也是固定的,方法有什么注解也是固定的。如果在调用时频繁去使用反射遍历所有的方法来获取异常处理方法,是不是不太合理?首先,@Controller类很多时候都没有异常处理方法,做这个遍历操作纯粹是无用功。其次,即使有异常处理方法,每次都遍历所有方法也不合理,应该缓存起来。因此运行时类一般是不会变的。
Spring的设计
因为不管是@ControllerAdvice还是@Controller,解析@ExceptionHandler的方式都是一样的,都要遍历所有方法来寻找。因此可以统一起来。于是抽象出来ExceptionHandlerMethodResolver。先说明,别搞混了哈,我们今天说的ExceptionHandlerExceptionResolver是负责调用ExceptionHandler方法来处理异常的ExceptionResolver[异常处理器],而这个ExceptionHandlerMethodResolver负责解析@ExceptionHandler的MethodResolver[方法解析器],
- 他负责解析管理类中的@ExceptionHandler方法。mappedMethods可以理解为其异常处理方法的注册中心。
- 由于异常可以嵌套,为了加速匹配,还搞了一个缓存exceptionLookupCache。该缓存使用的是Spring的ConcurrentReferenceHashMap,整个Entry都是软引用,即发生OOM异常之前,key、value都会被清理。(key是异常类型,value是异常处理方法)
ExceptionHandlerExceptionResolver则需要统筹之前说的@ControllerAdvice和@Controller的异常处理。
- exceptionHandlerCache
- key是handlerType(HandlerMethod对应的@Controller对象类型),value是与之对应的ExceptionHandlerMethodResolver
- exceptionHandlerAdviceCache
- 缓存@ControllerAdvice对象的ExceptionHandlerMethodResolver,
- key是ControllerAdviceBean(他实际上封装了@ControllerAdvice的类型信息,同时也可以通过beanFactory拿到相应的bean),value是与该对象对应的ExceptionHandlerMethodResolver
核心处理逻辑
核心处理逻辑在 ExceptionHandlerExceptionResolver#doResolveHandlerMethodException
/** * 寻找一个@ExceptionHandler方法并调用他处理抛出的异常 */ @Override @Nullable protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) { // 1. 获取匹配的异常处理方法,并封装成ServletInvocableHandlerMethod // 就是这个方法控制着优先使用@Controller中的@ExceptionHandler方法 // 他会首先检查exceptionHandlerCache,然后才到exceptionHandlerAdviceCache。他们都是通过ExceptionHandler**Method**Resolver找到目标方法的。 ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception); // 没有异常处理方法,则直接退出 if (exceptionHandlerMethod == null) { return null; } // 初始化exceptionHandlerMethod。主要是设置参数解析器、返回值处理器 // 省略... // 2. 准备exceptionHandlerMethod的调用参数 ServletWebRequest webRequest = new ServletWebRequest(request, response); ModelAndViewContainer mavContainer = new ModelAndViewContainer(); ArrayList<Throwable> exceptions = new ArrayList<>(); try { // 遍历嵌套异常作为方法参数 Throwable exToExpose = exception; while (exToExpose != null) { exceptions.add(exToExpose); Throwable cause = exToExpose.getCause(); exToExpose = (cause != exToExpose ? cause : null); } Object[] arguments = new Object[exceptions.size() + 1]; exceptions.toArray(arguments); // efficient arraycopy call in ArrayList arguments[arguments.length - 1] = handlerMethod; // 调用exceptionHandlerMethod处理异常 // 该方法的调用就跟RequestMappingHandlerAdapter是一样的了 exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments); } catch (Throwable invocationEx) { // 继续默认的异常处理 return null; } if (mavContainer.isRequestHandled()) { // 表示异常已被处理完成 return new ModelAndView(); } else { // 从ModelAndViewContainer封装ModelAndView返回 // 省略... return mav; } }
总结
- 异常处理会优先使用对应的@Controller中的@ExceptionHandler方法,然后才是@ControllerAdvice中的异常处理方法。
- 从宏观层面,@ExceptionHandler的缓存分为两层
层次 | 缓存所在 | 注册中心或缓存 | 描述 |
类层面 | ExceptionHandlerExceptionResolver | 管理@ControllerAdvice的exceptionHandlerAdviceCache以及管理@Controller的exceptionHandlerCache | 每个BeanType对应一个ExceptionHandlerMethodResolver |
方法层 | ExceptionHandlerMethodResolver | 管理@ExceptionHandler方法的mappedMethods。负责加速寻找处理方法的exceptionLookupCache | 每个类都可能有多个@ExceptionHandler |
- 方法调用与RequestMappingHandlerAdapter一样,都是通过ServletInvocableHandlerMethod进行处理。
如果理解了RequestMappingHandlerAdapter那么再来理解这个ExceptionHandlerExceptionResolver应该相对简单些,只需要重点理解两个点:
- @ExceptionHandler的出现的位置:@ControllerAdvice和@Controller。
- @ExceptionHandler的分层设计。
到此这篇关于SpringMVC的组件之HandlerExceptionResolver详解的文章就介绍到这了,更多相关HandlerExceptionResolver详解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!