前言
在介绍完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;
}
}
全貌-UML类图
最后给大家补一张全景图:ExceptionHandlerExceptionResolver的UML类图
总结
-
异常处理会优先使用对应的@Controller中的@ExceptionHandler方法,然后才是@ControllerAdvice中的异常处理方法。
-
从宏观层面,@ExceptionHandler的缓存分为两层
层次 缓存所在 注册中心或缓存 描述 类层面 ExceptionHandlerExceptionResolver 管理@ControllerAdvice的exceptionHandlerAdviceCache以及管理@Controller的exceptionHandlerCache 每个BeanType对应一个ExceptionHandlerMethodResolver 方法层 ExceptionHandlerMethodResolver 管理@ExceptionHandler方法的mappedMethods。负责加速寻找处理方法的exceptionLookupCache 每个类都可能有多个@ExceptionHandler -
方法调用与RequestMappingHandlerAdapter一样,都是通过ServletInvocableHandlerMethod进行处理。
后记
如果理解了RequestMappingHandlerAdapter那么再来理解这个ExceptionHandlerExceptionResolver应该相对简单些,只需要重点理解两个点:
- @ExceptionHandler的出现的位置:@ControllerAdvice和@Controller。
- @ExceptionHandler的分层设计。
好了,下回咱们该聊聊ViewResovler了。
上一篇:
探索SpringMVC-HandlerMapping之RequestMappingHandlerMapping
第一篇:
探索SpringMVC-web上下文
202301112008-GP