天天看點

源碼角度深度解析Spring的異常處理ExceptionHandler的實作原理

作者:搬山道猿

ExceptionHandler的作用

ExceptionHandler是Spring架構提供的一個注解,用于處理應用程式中的異常。當應用程式中發生異常時,ExceptionHandler将優先地攔截異常并處理它,然後将處理結果傳回到前端。該注解可用于類級别和方法級别,以捕獲不同級别的異常。

在Spring中使用ExceptionHandler非常簡單,隻需在需要捕獲異常的方法上注解@ExceptionHandler,然後定義一個方法,該方法将接收異常并傳回異常資訊,并将該異常資訊展示給前端使用者。

ExceptionHandler的使用

說明:針對可能出問題的Controller,新增注解方法@ExceptionHandler,下面是一個基本的ExceptionHandler示例:
@RestController
public 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方法。

源碼角度深度解析Spring的異常處理ExceptionHandler的實作原理

注冊的第一個HandlerExceptionResolver.ExceptionHandlerExceptionResolver, 繼承關系如下面所示。

源碼角度深度解析Spring的異常處理ExceptionHandler的實作原理

原理說明-AbstractHandlerExceptionResolver

代碼片段位于:org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver#resolveException
源碼角度深度解析Spring的異常處理ExceptionHandler的實作原理

這裡AbstractHandlerExceptionResolver的shouldApplyTo都傳回true, logException用來記錄日志、prepareResponse方法,用來設定response的Cache-Control。

異常處理方法就位于doResolveException

源碼角度深度解析Spring的異常處理ExceptionHandler的實作原理
源碼角度深度解析Spring的異常處理ExceptionHandler的實作原理
注意: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。

源碼角度深度解析Spring的異常處理ExceptionHandler的實作原理

@ExceptionHandler方法傳回值常見的可以是: ModelAndView 、@ResponseBody注解、ResponseEntity。

getExceptionHandlerMethod方法

getExceptionHandlerMethod說明: 擷取對應的@ExceptionHandler方法,封裝成ServletInvocableHandlerMethod傳回。

exceptionHandlerCache是針對Controller層面的@ExceptionHandler的處理方式,而exceptionHandlerAdviceCache是針對@ControllerAdvice的處理方式. 這兩個屬性都位于ExceptionHandlerExceptionResolver中。

源碼角度深度解析Spring的異常處理ExceptionHandler的實作原理

ExceptionHandlerMethodResolver,緩存A之前沒存儲過Controller的class ,是以建立一個ExceptionHandlerMethodResolver 加入緩存中,ExceptionHandlerMethodResolver 的初始化工作一定做了某些工作。

源碼角度深度解析Spring的異常處理ExceptionHandler的實作原理

resolveMethod方法

根據異常對象讓 ExceptionHandlerMethodResolver 解析得到 method , 比對到異常處理方法就直接封裝成對象 ServletInvocableHandlerMethod ; 就不會再去走@ControllerAdvice裡的異常處理器了,這裡說明了。

源碼角度深度解析Spring的異常處理ExceptionHandler的實作原理

resolveMethodByExceptionType根據目前抛出異常尋找 比對的方法,并且做了緩存,以後遇到同樣的異常可以直接走緩存取出

源碼角度深度解析Spring的異常處理ExceptionHandler的實作原理

resolveMethodByExceptionType方法,嘗試從緩存A:exceptionLookupCache中根據異常class類型擷取Method ,初始時候肯定緩存為空 ,就去周遊ExceptionHandlerMethodResolver的mappedMethods(上面提及了key為異常類型,value為method,exceptionType為目前@RequestMapping方法抛出的異常,判斷目前異常類型是不是@ExceptionHandler中value聲明的子類或本身,滿足條件就代表比對上了;

源碼角度深度解析Spring的異常處理ExceptionHandler的實作原理

可能存在多個比對的方法,使用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等多種形式。

@ControllerAdvice
public 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
源碼角度深度解析Spring的異常處理ExceptionHandler的實作原理

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實作類,多少有點性能損耗.