Spring 異常處理三種方式 @ExceptionHandler


  1. 異常處理方式一. @ExceptionHandler
  2. 異常處理方式二. 實現HandlerExceptionResolver接口
  3. 異常處理方式三. @ControllerAdvice+@ExceptionHandler
  4. 三種方式比較說明(強烈推薦各位看一下,我覺得自己總結的比較多,嘿嘿,不對之處請指出,點我快速前往!)

 

問題描述: 假如對異常不進行處理?

假如SpringMvc我們不對異常進行任何處理, 界面上顯示的是這樣的.

image

 

異常處理的方式有三種:

一. Controller層面上異常處理 @ExceptionHandler

說明:針對可能出問題的Controller,新增注解方法@ExceptionHandler.

@Controller
@RequestMapping("/testController")
public class TestController {

    @RequestMapping("/demo1")
    @ResponseBody
    public Object demo1(){
        int i = 1 / 0;
        return new Date();
    }

    @ExceptionHandler({RuntimeException.class})
    public ModelAndView fix(Exception ex){
        System.out.println("do This");
        return new ModelAndView("error",new ModelMap("ex",ex.getMessage()));
    }
}

注意事項: 1. 一個Controller下多個@ExceptionHandler上的異常類型不能出現一樣的,否則運行時拋異常.

Ambiguous @ExceptionHandler method mapped for;

             2. @ExceptionHandler下方法返回值類型支持多種,常見的ModelAndView,@ResponseBody注解標注,ResponseEntity等類型都OK.

 

戳我跳過原理,查看異常捕獲方式二.

 

原理說明:

代碼片段位於:org.springframework.web.servlet.DispatcherServlet#doDispatch

執行@RequestMapping方法拋出異常后,Spring框架 try-catch的方法捕獲異常,  正常邏輯發不發生異常都會走processDispatchResult流程 ,區別在於異常的參數是否為null .

HandlerExecutionChain mappedHandler = null;
	Exception dispatchException = null;
	ModelAndView mv = null;
	try{
		
		mappedHandler=getHandler(request); //根據請求查找handlerMapping找到controller
		HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());//找到處理器適配器HandlerAdapter
		
		if(!mappedHandler.applyPreHandle(request,response)){ //攔截器preHandle
			return ;
		}		
		mv=ha.handle(request,response);	//調用處理器適配器執行@RequestMapping方法
		mappedHandler.applyPostHandle(request,response,mv);  //攔截器postHandle
	}catch(Exception ex){
		dispatchException=ex;
	}
		processDispatchResult(request,response,mappedHandler,mv,dispatchException) //將異常信息傳入了

 

 

代碼片段位於:org.springframework.web.servlet.DispatcherServlet#processDispatchResult

如果@RequestMapping方法拋出異常,攔截器的postHandle方法不執行,進入 processDispatchResult,判斷入參 dispatchException,不為null , 代表發生異常,調用processHandlerException處理,

image

 

代碼片段位於:org.springframework.web.servlet.DispatcherServlet#processHandlerException

this當前對象指dispatchServlet,handlerExceptionResolvers可以看到有三個HandlerExceptionResolver, 這三個是<mvc:annotation-driven />幫我們注冊的.  遍歷有序集合handlerExceptionResolvers,調用接口的resolveException方法. 

image

 

記錄<mvc:annotation-driven/>注冊的第一個 HandlerExceptionResolver : ExceptionHandlerExceptionResolver,  繼承關系如下面所示.

image

 

代碼片段位於:org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver#resolveException

AbstractHandlerExceptionResolver 和 AbstractHandlerMethodExceptionResolver名字看起來非常相似. 這里AbstractHandlerExceptionResolver 的shouldApplyTo都返回 true, logException用來記錄日志、prepareResponse方法用來設置response的Cache-Control.    異常處理方法就位於doResolveException.

 

image

 

代碼片段位於:org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver#shouldApplyTo

接口方法實現是AbstractHandlerExceptionResolver的resolveException,先判斷 shouldApplyTo, AbstractHandlerExceptionResolver 和子類AbstractHandlerMethodExceptionResolver都實現了shouldApplyTo方法,子類的shouldApplyTo都調用父類AbstractHandlerExceptionResolver的shouldApplyTo.

image

 

查看父類AbstractHandlerExceptionResolver的shouldApplyTo方法.

代碼片段位於:org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver#shouldApplyTo

Spring初始化的時候並沒有額外配置 , 所以mappedHandlers和mappedHandlerClasses都為null,  可以在這塊擴展進行篩選  ,AbstractHandlerExceptionResolver提供了  setMappedHandlerClasses 、setMappedHandlers用於擴展.

image

 

代碼片段位於:org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver#doResolveException

image

 

代碼片段位於:org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#doResolveHandlerMethodException

似曾相識的ServletInvocableHandlerMethod,getExceptionHandlerMethod目的就是獲取 針對異常的處理方法,沒找到的話這里就直接返回了,找到了執行異常處理方法;

之后同Spring請求方法執行一樣的處理方式,設置argumentResolvers、returnValueHandlers,之后進行調用異常處理方法,

@ExceptionHandler的方法入參支持:Exception ;SessionAttribute 、 RequestAttribute注解 ; HttpServletRequest  、HttpServletResponse、HttpSession.

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

image

 

getExceptionHandlerMethod說明: 獲取對應的@ExceptionHandler方法,封裝成ServletInvocableHandlerMethod返回.

代碼片段位於:org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#getExceptionHandlerMethod

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

handlerType指代Controller的class屬性,嘗試從緩存A exceptionHandlerCache 中根據controller的class  查找ExceptionHandlerMethodResolver;  緩存A之前沒存儲過Controller的class  ,所以新建一個ExceptionHandlerMethodResolver 加入緩存中.  ExceptionHandlerMethodResolver 的初始化工作一定做了某些工作!

resolveMethod方法:根據異常對象讓  ExceptionHandlerMethodResolver 解析得到 method , 匹配到異常處理方法  就直接封裝成對象 ServletInvocableHandlerMethod ; 就不會再去走@ControllerAdvice里的異常處理器了.  這里說明了,ExceptionHandlerMethodResolver 初始化的時候完成存儲 @ExceptionHandler.

image

 

查看ExceptionHandlerMethodResolver 初始化工作內容:

代碼片段位於:org.springframework.web.method.annotation.ExceptionHandlerMethodResolver#ExceptionHandlerMethodResolver

handlerType為傳入的Controller的class屬性,通過EXCEPTION_HANDLER_METHODS選出 class 中標注@ExceptionHandler的方法,解析@Exception注解的value值(class類型的數組),並加入到當前ExceptionHandlerMethodResolvermappedMethods集合中,key為 異常類型 ,value為 method.

如果@ExceptionHandler的 value屬性為空,就會將方法入參中的Throwable的子類作為異常類型.  @ExceptionHandler的value屬性和方法入參不能同時都為空,否則會拋出異常.

image

image

image

image

 

ExceptionHandlerMethodResolver完成了初始化工作,如何根據當前發生異常類型查找到對應方法?

代碼片段位於:org.springframework.web.method.annotation.ExceptionHandlerMethodResolver#resolveMethod

resolveMethodByExceptionType根據當前拋出異常尋找 匹配的方法,並且做了緩存,以后遇到同樣的異常可以直接走緩存取出method,

 

image

 

代碼片段位於:org.springframework.web.method.annotation.ExceptionHandlerMethodResolver#resolveMethodByExceptionType

resolveMethodByExceptionType方法,嘗試從緩存A:exceptionLookupCache中根據 異常class類型獲取Method ,初始時候肯定緩存為空 ,就去 遍歷ExceptionHandlerMethodResolvermappedMethods(上面提及了key為異常類型,value為method),  exceptionType為當前@RequestMapping方法拋出的異常,判斷當前異常類型是不是@ExceptionHandler中value聲明的子類或本身,滿足條件就代表匹配上了;可能存在多個匹配的方法,使用ExceptionDepthComparator排序,排序規則是按照繼承順序來(繼承關系越靠近數值越小,當前類最小為0,頂級父類Throwable為int最大值),排序之后選取繼承關系最靠近的那個,並且存入ExceptionHandlerMethodResolverexceptionLookupCache中,key為當前拋出的異常,value為解析出來的匹配method.

image

 

至此 @ExceptionHandler Spring讀取到並解析出來完畢了,后續流程和Spring正常請求流程一樣,包括@ExceptionHandler的方法入參、方法返回值.

@ExceptionHandler的方法入參支持:Exception ;SessionAttribute 、 RequestAttribute注解 ; HttpServletRequest  、HttpServletResponse、HttpSession.

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

 

 

二. 全局級別異常處理器 實現HandlerExceptionResolver接口

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());
        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();
    }

}

 

戳我跳過原理解釋,查看方式三.

 

 

原理分析:記得之前介紹了 DispatcherServlet的HandlerExceptionResolver集合,這種方式的HandlerExceptionResolver就是從DispatcherServlet的HandlerExceptionResolver集合入手的.

代碼片段位於:org.springframework.web.servlet.DispatcherServlet#processHandlerException

this對象指代DispatcherServlet,和上面方式對比,發現我們只是將MyHandlerExceptionResolver 加入到Spring容器,dispatchServlet 的 handlerExceptionResolvers屬性就多了我們自己定義的全局異常解析器;

ExceptionHandlerMethodResolver是用來解析@Controller層面的@ExceptionHandler注解,當前Controller沒有找到@ExceptionHandler來處理自己拋出的異常,才遍歷下一個HandlerExceptionResolver;

HandlerExceptionResolver是個有序集合,Spring注冊的HandlerExceptionResolver調用resolveException都失敗之后,才輪到我們自定義的MyHandlerExceptionResolver ;而且我們自定義的MyHandlerExceptionResolver 就沒法使用SpringMvc的注解等等.

image

 

我們只是將HandlerExceptionResolver加入到Spring容器中,Spring是如何通知給DispatcherServlet呢?

代碼片段位於:org.springframework.web.servlet.DispatcherServlet#initHandlerExceptionResolvers

initHandlerExceptionResolvers只是DispatcherServlet初始化策略方法initStrategies中的一小步,可以看到只要是SpringMvc父子容器中注冊的HandlerExceptionResolver類型實例,DispatcherServlet都會自動將其加入到DispatcherServlet的handlerExceptionResolvers中. 所以我們需要做的只是實現HandlerExceptionResolver接口,並且納入Spring容器管理即可.

image

 

三.全局級別異常處理器 @ControllerAdvice+@ExceptionHandler

簡單使用方法:

@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);
    }
}

用法說明: 這種情況下 @ExceptionHandler 與第一種方式用法相同,返回值支持ModelAndView,@ResponseBody等多種形式.

 

方式一提到ExceptionHandlerExceptionResolver不僅維護@Controller級別的@ExceptionHandler,同時還維護的@ControllerAdvice級別的@ExceptionHandler.

代碼片段位於:org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#getExceptionHandlerMethod

  isApplicableToBeanType方法是用來做條件判斷的,@ControllerAdvice注解有很多屬性用來設置條件,basePackageClasses、assignableTypes、annotations等,比如我限定了annotations為注解X, 那標注了@X 的ControllerA就可以走這個異常處理器,ControllerB就不能走這個異常處理器.

現在問題的關鍵就只剩下了exceptionHandlerAdviceCache是什么時候掃描@ControllerAdvice的,下面的邏輯和@ExceptionHandler的邏輯一樣了.

image

 

exceptionHandlerAdviceCache初始化邏輯:

代碼片段位於:org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#afterPropertiesSet

afterPropertiesSet是Spring bean創建過程中一個重要環節. 

image

 

代碼片段位於:org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#initExceptionHandlerAdviceCache

ControllerAdviceBean.findAnnotatedBeans方法查找了SpringMvc父子容器中標注 @ControllerAdvice 的bean, new ExceptionHandlerMethodResolver初始化時候解析了當前的@ControllerAdvice的bean的@ExceptionHandler,加入到ExceptionHandlerExceptionResolverexceptionHandlerAdviceCache中,key為ControllerAdviceBean,value為ExceptionHandlerMethodResolver . 到這里exceptionHandlerAdviceCache就初始化完畢.

image

 

查找SpringMvc父子容器中所有@ControllerAdivce的bean的方法

代碼片段位於:org.springframework.web.method.ControllerAdviceBean#findAnnotatedBeans

遍歷了SpringMVC父子容器中所有的bean,標注ControllerAdvice注解的bean加入集合返回.

image

 

 

四.比較說明.

@Controller+@ExceptionHandler、HandlerExceptionResolver接口形式、@ControllerAdvice+@ExceptionHandler優缺點說明:

在Spring4.3.0版本下,1.優先級來說,@Controller+@ExceptionHandler優先級最高,其次是@ControllerAdvice+@ExceptionHandler,最后才是HandlerExceptionResolver,說明假設三種方式並存的情況 優先級越高的越先選擇,而且被一個捕獲處理了就不去執行其他的.

                                2. 三種方式都支持多種返回類型,@Controller+@ExceptionHandler、@ControllerAdvice+@ExceptionHandler可以使用Spring支持的@ResponseBody、ResponseEntity,而HandlerExceptionResolver方法聲明返回值類型只能是 ModelAndView,如果需要返回JSON、xml等需要自己實現.

                                3.緩存利用,@Controller+@ExceptionHandler的緩存信息在ExceptionHandlerExceptionResolver的exceptionHandlerCache,@ControllerAdvice+@ExceptionHandler的緩存信息在ExceptionHandlerExceptionResolver的exceptionHandlerAdviceCache中, 而HandlerExceptionResolver接口是不做緩存的,在前面兩種方式都fail的情況下才會走自己的HandlerExceptionResolver實現類,多少有點性能損耗.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM