写点什么

【Spring 专题】「技术原理」从源码角度去深入分析关于 Spring 的异常处理 ExceptionHandler 的实现原理

作者:洛神灬殇
  • 2023-04-09
    江苏
  • 本文字数:5556 字

    阅读完需:约 18 分钟

【Spring专题】「技术原理」从源码角度去深入分析关于Spring的异常处理ExceptionHandler的实现原理

ExceptionHandler 的作用

ExceptionHandler 是 Spring 框架提供的一个注解,用于处理应用程序中的异常。当应用程序中发生异常时,ExceptionHandler 将优先地拦截异常并处理它,然后将处理结果返回到前端。该注解可用于类级别和方法级别,以捕获不同级别的异常。


在 Spring 中使用 ExceptionHandler 非常简单,只需在需要捕获异常的方法上注解 @ExceptionHandler,然后定义一个方法,该方法将接收异常并返回异常信息,并将该异常信息展示给前端用户。

ExceptionHandler 的使用

说明:针对可能出问题的 Controller,新增注解方法 @ExceptionHandler,下面是一个基本的 ExceptionHandler 示例


@RestControllerpublic class ExceptionController {      @ExceptionHandler(Exception.class)    public ResponseEntity<String> handleException(Exception ex) {        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)                .body("An error occurred: " + ex.getMessage());    }    @RequestMapping("/test")    public String test() throws Exception {        throw new Exception("Test exception!");    }}
复制代码


在上面的示例中,我们定义了一个叫做 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 .


  HandlerExecutionChain mappedHandler = null;  Exception dispatchException = null;  ModelAndView mv = null;    try{        //根据请求查找handlerMapping找到controller        mappedHandler=getHandler(request);         //找到处理器适配器HandlerAdapter        HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());         if(!mappedHandler.applyPreHandle(request,response)){             //拦截器preHandle            return ;        }              //调用处理器适配器执行@RequestMapping方法        mv=ha.handle(request,response);         //拦截器postHandle        mappedHandler.applyPostHandle(request,response,mv);      }catch(Exception ex){        dispatchException=ex;    }    //将异常信息传入了    processDispatchResult(request,response,mappedHandler,mv,dispatchException) 
复制代码

原理说明-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 接口

public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { ModelMap mmp=new ModelMap(); mmp.addAttribute("ex",ex.getMessage()); return new ModelAndView("error",mmp); }}
复制代码


  • 使用方式: 只需要将该 Bean 加入到 Spring 容器,可以通过 Xml 配置,也可以通过注解方式加入容器;

  • 方法返回值不为 null 才有意义,如果方法返回值为 null,可能异常就没有被捕获.

  • 缺点分析:比如这种方式全局异常处理返回 JSP、velocity 等视图比较方便,返回 json 或者 xml 等格式的响应就需要自己实现了.如下是我实现的发生全局异常返回 JSON 的简单例子.


public class MyHandlerExceptionResolver implements HandlerExceptionResolver {    @Override    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {        System.out.println("发生全局异常!");        ModelMap mmp=new ModelMap();        mmp.addAttribute("ex",ex.getMessage());        response.addHeader("Content-Type","application/json;charset=UTF-8");        try {            new ObjectMapper().writeValue(response.getWriter(),ex.getMessage());            response.getWriter().flush();        } catch (IOException e) {            e.printStackTrace();        }        return new ModelAndView();    }}
复制代码

全局级别异常处理器 @ControllerAdvice+@ExceptionHandler 使用方法

用法说明:这种情况下 @ExceptionHandler 与第一种方式用法相同,返回值支持 ModelAndView,@ResponseBody 等多种形式。


@ControllerAdvicepublic class GlobalController {    @ExceptionHandler(RuntimeException.class)    public ModelAndView fix1(Exception e){        System.out.println("全局的异常处理器");        ModelMap mmp=new ModelMap();        mmp.addAttribute("ex",e);        return new ModelAndView("error",mmp);    }}
复制代码


  • 方式一:提到 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 实现类,多少有点性能损耗.

用户头像

洛神灬殇

关注

🏆 InfoQ写作平台-签约作者 🏆 2020-03-25 加入

【个人简介】酷爱计算机科学、醉心编程技术、喜爱健身运动、热衷悬疑推理的“极客达人” 【技术格言】任何足够先进的技术都与魔法无异 【技术范畴】Java领域、Spring生态、MySQL专项、微服务/分布式体系和算法设计等

评论

发布
暂无评论
【Spring专题】「技术原理」从源码角度去深入分析关于Spring的异常处理ExceptionHandler的实现原理_spring_洛神灬殇_InfoQ写作社区