【Spring 专题】「技术原理」从源码角度去深入分析关于 Spring 的异常处理 ExceptionHandler 的实现原理
ExceptionHandler 的作用
ExceptionHandler 是 Spring 框架提供的一个注解,用于处理应用程序中的异常。当应用程序中发生异常时,ExceptionHandler 将优先地拦截异常并处理它,然后将处理结果返回到前端。该注解可用于类级别和方法级别,以捕获不同级别的异常。
在 Spring 中使用 ExceptionHandler 非常简单,只需在需要捕获异常的方法上注解 @ExceptionHandler,然后定义一个方法,该方法将接收异常并返回异常信息,并将该异常信息展示给前端用户。
ExceptionHandler 的使用
说明:针对可能出问题的 Controller,新增注解方法 @ExceptionHandler,下面是一个基本的 ExceptionHandler 示例:
在上面的示例中,我们定义了一个叫做 ExceptionController 的类,该类是一个**@RestController**注解的控制器,它包括一个可以产生异常的请求处理程序,一个用于捕获和处理异常的 @ExceptionHandler 方法。
@RequestMapping 注解配置了一个名为“/test”的 API,该 API 将抛出一个异常,该异常将由我们上面的 ExceptionHandler 进行处理。当请求“/test”时,Controller 方法将引发异常并触发 @ExceptionHandler 方法。
在上面的 @ExceptionHandler 方法中,我们通过 ResponseEntity 将异常信息提供给客户端,HTTP 状态码设置为 500。这使客户端了解已发生错误,并能够在日志中记录异常信息以便日后调试。
总之,使用 ExceptionHandler 能够更好的掌控应用的异常信息,使得应用在发生异常的时候更加可控,并且更加容易进行调试。
ExceptionHandler 的注意事项
Controller 类下多个**@ExceptionHandler**上的异常类型不能出现一样的,否则运行时抛异常。
@ExceptionHandler 下方法返回值类型支持多种,常见的 ModelAndView,@ResponseBody 注解标注,ResponseEntity 等类型都 OK.
源码分析介绍
原理说明-doDispatch
代码片段位于:org.springframework.web.servlet.DispatcherServlet#doDispatch
执行**@RequestMapping 方法抛出异常后,Spring 框架 try-catch 的方法捕获异常, 正常逻辑发不发生异常都会走 processDispatchResult**流程 ,区别在于异常的参数是否为 null .
原理说明-processDispatchResult
代码片段位于:org.springframework.web.servlet.DispatcherServlet#processDispatchResult
如果 @RequestMapping 方法抛出异常,拦截器的 postHandle 方法不执行,进入 processDispatchResult,判断入参 dispatchException,不为 null , 代表发生异常,调用 processHandlerException 处理。
原理说明-processHandlerException
代码片段位于:org.springframework.web.servlet.DispatcherServlet#processHandlerException
this 当前对象指 dispatchServlet,handlerExceptionResolvers 可以看到三个 HandlerExceptionResolver,这三个是 Spring 框架帮我们注册的,遍历有序集合 handlerExceptionResolvers,调用接口的 resolveException 方法。
注册的第一个 HandlerExceptionResolver.ExceptionHandlerExceptionResolver, 继承关系如下面所示。
原理说明-AbstractHandlerExceptionResolver
代码片段位于:org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver#resolveException
这里 AbstractHandlerExceptionResolver 的 shouldApplyTo 都返回 true, logException 用来记录日志、prepareResponse 方法,用来设置 response 的 Cache-Control。
异常处理方法就位于 doResolveException
注意:AbstractHandlerExceptionResolver 和 AbstractHandlerMethodExceptionResolver 名字看起来非常相似,但是作用不同,一个是面向整个类的,一个是面向方法级别的。
原理说明-AbstractHandlerMethodExceptionResolver
代码片段位于:org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver#shouldApplyTo
接口方法实现 AbstractHandlerExceptionResolver 的 resolveException,先判断 shouldApplyTo,AbstractHandlerExceptionResolver 和子类 AbstractHandlerMethodExceptionResolver 都实现了 shouldApplyTo 方法,子类的 shouldApplyTo 都调用父类 AbstractHandlerExceptionResolver 的 shouldApplyTo.
父类 AbstractHandlerExceptionResolver 的 shouldApplyTo 方法.
代码片段位于:org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver#shouldApplyTo
Spring 初始化的时候并没有额外配置 , 所以 mappedHandlers 和 mappedHandlerClasses 都为 null, 可以在这块扩展进行筛选 ,AbstractHandlerExceptionResolver 提供了 setMappedHandlerClasses 、setMappedHandlers 用于扩展。
doResolveException
代码片段位于:org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver#doResolveException
Spring 请求方法执行一样的处理方式,设置 argumentResolvers、returnValueHandlers,之后进行调用异常处理方法。
获取 @ExceptionHandler
@ExceptionHandler 的方法入参支持:Exception ;SessionAttribute 、 RequestAttribute 注解、 HttpServletRequest 、HttpServletResponse、HttpSession。
@ExceptionHandler 方法返回值常见的可以是: ModelAndView 、@ResponseBody 注解、ResponseEntity。
getExceptionHandlerMethod 方法
getExceptionHandlerMethod 说明: 获取对应的 @ExceptionHandler 方法,封装成 ServletInvocableHandlerMethod 返回。
exceptionHandlerCache 是针对 Controller 层面的 @ExceptionHandler 的处理方式,而 exceptionHandlerAdviceCache 是针对 @ControllerAdvice 的处理方式. 这两个属性都位于 ExceptionHandlerExceptionResolver 中。
ExceptionHandlerMethodResolver,缓存 A 之前没存储过 Controller 的 class ,所以新建一个 ExceptionHandlerMethodResolver 加入缓存中,ExceptionHandlerMethodResolver 的初始化工作一定做了某些工作。
resolveMethod 方法
根据异常对象让 ExceptionHandlerMethodResolver 解析得到 method , 匹配到异常处理方法就直接封装成对象 ServletInvocableHandlerMethod ; 就不会再去走 @ControllerAdvice 里的异常处理器了,这里说明了。
resolveMethodByExceptionType 根据当前抛出异常寻找 匹配的方法,并且做了缓存,以后遇到同样的异常可以直接走缓存取出
resolveMethodByExceptionType 方法,尝试从缓存 A:exceptionLookupCache 中根据异常 class 类型获取 Method ,初始时候肯定缓存为空 ,就去遍历 ExceptionHandlerMethodResolver 的 mappedMethods(上面提及了 key 为异常类型,value 为 method,exceptionType 为当前 @RequestMapping 方法抛出的异常,判断当前异常类型是不是 @ExceptionHandler 中 value 声明的子类或本身,满足条件就代表匹配上了;
可能存在多个匹配的方法,使用 ExceptionDepthComparator 排序,排序规则是按照继承顺序来(继承关系越靠近数值越小,当前类最小为 0,顶级父类 Throwable 为 int 最大值),排序之后选取继承关系最靠近的那个,并且 ExceptionHandlerMethodResolver 的 exceptionLookupCache 中,key 为当前抛出的异常,value 为解析出来的匹配 method.
全局级别异常处理器实现 HandlerExceptionResolver 接口
使用方式: 只需要将该 Bean 加入到 Spring 容器,可以通过 Xml 配置,也可以通过注解方式加入容器;
方法返回值不为 null 才有意义,如果方法返回值为 null,可能异常就没有被捕获.
缺点分析:比如这种方式全局异常处理返回 JSP、velocity 等视图比较方便,返回 json 或者 xml 等格式的响应就需要自己实现了.如下是我实现的发生全局异常返回 JSON 的简单例子.
全局级别异常处理器 @ControllerAdvice+@ExceptionHandler 使用方法
用法说明:这种情况下 @ExceptionHandler 与第一种方式用法相同,返回值支持 ModelAndView,@ResponseBody 等多种形式。
方式一:提到 ExceptionHandlerExceptionResolver 不仅维护 @Controller 级别的 @ExceptionHandler,同时还维护的 @ControllerAdvice 级别的 @ExceptionHandler 代码片段位于:
isApplicableToBeanType 方法是用来做条件判断的,@ControllerAdvice 注解有很多属性用来设置条件,basePackageClasses、assignableTypes、annotations 等,比如我限定了 annotations 为注解 X, 那标注了 @X 的 ControllerA 就可以走这个异常处理器,ControllerB 就不能走这个异常处理器。
现在问题的关键就只剩下了 exceptionHandlerAdviceCache 是什么时候扫描 @ControllerAdvice 的,下面的逻辑和 @ExceptionHandler 的逻辑一样了,exceptionHandlerAdviceCache 初始化逻辑:
代码片段位于:org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#afterPropertiesSet,afterPropertiesSet 是 Spring bean 创建过程中一个重要环节。
代码片段位于:org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#initExceptionHandlerAdviceCache
ControllerAdviceBean.findAnnotatedBeans 方法查找了 SpringMvc 父子容器中标注 @ControllerAdvice 的 bean, new ExceptionHandlerMethodResolver 初始化时候解析了当前的 @ControllerAdvice 的 bean 的 @ExceptionHandler,加入到 ExceptionHandlerExceptionResolver 的 exceptionHandlerAdviceCache 中,key 为 ControllerAdviceBean,value 为 ExceptionHandlerMethodResolver . 到这里 exceptionHandlerAdviceCache 就初始化完毕。
Spring 父子容器中所有 @ControllerAdivce 的 bean 的方法
代码片段位于:org.springframework.web.method.ControllerAdviceBean#findAnnotatedBeans
遍历了 SpringMVC 父子容器中所有的 bean,标注 ControllerAdvice 注解的 bean 加入集合返回。
比较说明
@Controller+@ExceptionHandler、HandlerExceptionResolver 接口形式、@ControllerAdvice+@ExceptionHandler 优缺点说明:
调用优先级
@Controller+@ExceptionHandler 优先级最高
@ControllerAdvice+@ExceptionHandler 略低
HandlerExceptionResolver 最低。
三种方式并存的情况 优先级越高的越先选择,而且被一个捕获处理了就不去执行其他的。
三种方式都支持多种返回类型
@Controller+@ExceptionHandler、@ControllerAdvice+@ExceptionHandler 可以使用 Spring 支持的 @ResponseBody、ResponseEntity。
HandlerExceptionResolver 方法声明返回值类型只能是 ModelAndView,如果需要返回 JSON、xml 等需要自己实现.。
缓存利用
@Controller+@ExceptionHandler 的缓存信息在 ExceptionHandlerExceptionResolver 的 exceptionHandlerCache,@ControllerAdvice+@ExceptionHandler 的缓存信息在 ExceptionHandlerExceptionResolver 的 exceptionHandlerAdviceCache 中,
HandlerExceptionResolver 接口是不做缓存的,在异常报错的情况下才会走自己的 HandlerExceptionResolver 实现类,多少有点性能损耗.
评论