本文涉及SpringMVC異常處理體系源碼分析,SpringMVC異常處理相關類的設計模式,實際工做中異常處理的實踐。html
假設咱們的SpringMVC應用中有以下控制器:java
代碼示例-1 @RestController("/order") public class OrderController{ @RequestMapping("/detail") public Object orderDetail(int orderId){ // ... } }
這個控制器中接收了一個參數:int 類型的orderId。假設我在請求的使傳遞的參數爲orderId=99999999999或者orderId=53844181132132asdf。很顯然,咱們的第一個參數超出了int的範圍,第二個參數類型不符合。這時確定會報400錯誤,假設咱們的應用是部署在tomcat裏邊的,咱們會獲得的錯誤頁面是這樣的:算法
代碼示例-2 <html> <head><title>Apache Tomcat/7.0.42 - Error report</title> <style> <!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}A {color : black;}A.name {color : black;}HR {color : #525D76;}--> </style> </head> <body> <h1>HTTP Status 400 - </h1> <HR size="1" noshade="noshade"> <p><b>type</b> Status report</p> <p><b>message</b> <u></u></p> <p> <b>description</b> <u>The request sent by the client was syntactically incorrect.</u> </p><HR size="1" noshade="noshade"> <h3>Apache Tomcat/7.0.42</h3> </body> </html>
當咱們碰到這個錯的時候,實際上都沒有進入目標方法,控制檯也看不到controller方法執行的日誌相關信息。根據經驗,咱們知道這是請求錯誤,是請求參數不匹配致使的(實際拋出的異常是:org.springframework.beans.TypeMismatchException: Failed to convert value of type)。也許你會說解決這個問題,只須要傳遞正確的參數就能夠了,可是spring是怎麼處理這個錯誤的,流程是怎樣?若是瞭解這些,對於咱們解決問題更有幫助。spring
爲了追蹤處理過程,我會使用斷點調試的方式。咱們知道,SpringMVC的核心是DispatchServlet。全部的請求會被DispatchServlet接收,並在其doDispatch(...)方法中處理。doDispatch()方法會找到對應的handler,而後invoke。因此咱們在doDispatch方法中打個斷點。咱們使用postman發起一個請求,並傳遞一個錯誤的參數。先貼一點doDispatch()方法的代碼:json
代碼示例-3 protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { //刪除一些代碼 try { ModelAndView mv = null; Exception dispatchException = null; try { // 刪除一些代碼方便閱讀 try { // Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); } finally { if (asyncManager.isConcurrentHandlingStarted()) { return; } } applyDefaultViewName(request, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; // 這裏捕獲了異常TypeMismatchException } processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { } finally { // 刪除一些代碼 } }
當請求進入doDispatch()方法以後,單步執行發現,發生了一個異常,而後,這個異常被catch住了,catch塊裏邊進行了以下操做:設計模式
代碼示例-4 dispatchException = ex;
異常的詳細信息是:api
代碼示例-5 org.springframework.beans.TypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.lang.Integer'; nested exception is java.lang.NumberFormatException: For input string: "53844181132132asdf"
繼續執行,走出了catch塊以後,便進入了processDispatchResult方法:數組
代碼示例-5 private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception { boolean errorView = false; if (exception != null) { if (exception instanceof ModelAndViewDefiningException) { logger.debug("ModelAndViewDefiningException encountered", exception); mv = ((ModelAndViewDefiningException) exception).getModelAndView(); } else { Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); mv = processHandlerException(request, response, handler, exception);// 執行這個方法 errorView = (mv != null); } } // 方便閱讀,刪除了其餘代碼 }
這個方法中對異常進行判斷,發現不是「ModelAndViewDefiningException」就交給processHandlerException()方法繼續處理。processHandlerException方法代碼以下:spring-mvc
代碼示例-6 protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // Check registered HandlerExceptionResolvers... ModelAndView exMv = null; for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) { exMv = handlerExceptionResolver.resolveException(request, response, handler, ex); if (exMv != null) { break; } } // 去掉了一些代碼 throw ex; }
這裏的for循環是爲了找一個handler來處理這個異常。這裏的handler列表有:tomcat
在上面的代碼中,經過for循環須要在衆多的handler中找一個HandlerExceptionResolver的實現類來處理異常。這裏的handler列表是在應用初始化的時候就建立了,前三個是spring內部自帶的,後面是咱們自定義的(若是有的話)。處理異常的方法是resolveException(),它實際上是在HandlerExceptionResolver接口中定義的,該接口只有一個方法resolveException(),代碼以下:
代碼示例-7 public interface HandlerExceptionResolver { ModelAndView resolveException( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex); }
Spring自帶的ExceptionHandlerExceptionResolver,ResponseStatusExceptionResolver,DefaultHandlerExceptionResolver都是繼承自AbstractHandlerExceptionResolver類,這個類是一個抽象類,它實現了HandlerExceptionResolver接口,它對HandlerExceptionResolver接口約定的方法的所實現代碼是這樣的:
代碼示例-8 public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { if (shouldApplyTo(request, handler)) { logException(ex, request); prepareResponse(ex, response); return doResolveException(request, response, handler, ex); } else { return null; } }
這個方法實際上是一個模板,這裏使用的是模板方法設計模式。這個模板定義了處理異常的邏輯,return null或者進入if執行「三步走」,看上面代碼,這三步分別是:
這裏的第三部doResolveException(request, response, handler, ex)是一個抽象方法,它也是咱們的模板方法。它的聲明是這樣的:
代碼示例-9 protected abstract ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
這個抽象方法就是留個子類來實現的。模板我定好了,子類想咋處理就怎麼實現。不管你咋實現,反正我這「三步走」是已經定好的了。因此,模板方法設計模式就是這樣:「定義一個操做中的算法的骨架,而將一些步驟延遲到子類中。TemplateMethod使得子類能夠不改變一個算法的結構便可重定義該算法的某些特定步驟。」
繼續回到代碼邏輯,剛纔講到,咱們的for循環遍歷當前的handler,並調用當前handler的resolveException方法。正如 [代碼示例-8 ] 所示這個resolveException方法是個模板方法,它的第一步就是一個if判斷,這個判斷的方法代碼以下:
代碼示例-10 protected boolean shouldApplyTo(HttpServletRequest request, Object handler) { if (handler != null) { if (this.mappedHandlers != null && this.mappedHandlers.contains(handler)) { return true; } if (this.mappedHandlerClasses != null) { for (Class handlerClass : this.mappedHandlerClasses) { if (handlerClass.isInstance(handler)) { return true; } } } } return (this.mappedHandlers == null && this.mappedHandlerClasses == null); }
this.mappedHandlers 是一個 Set ,它存儲了當前異常處理器有哪些handler。若是這個set不爲空,而且包含了當前的目標handler,那就說明這個異常處理器能夠處理當前的目標handler。(這裏所說的handler其實就是controller的目標方法,以開篇的例子來講,這個handler類包含的信息、目標方法,總之handler指明咱們要調用的是OrderController類的orderDetail方法)。
因而,咱們的for循環,依次發現了ExceptionHandlerExceptionResolver不能處理,ResponseStatusExceptionResolver也不能處理,下一個輪到DefaultHandlerExceptionResolver的時候,能夠了,進入了if裏邊的「三步走」。最終執行了該類對模板方法doResolveException的實現代碼,這個代碼是這樣的:
代碼示例-11 @Override protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { if (ex instanceof NoSuchRequestHandlingMethodException) { return handleNoSuchRequestHandlingMethod(...); } // 刪除部分else if instanceof 判斷 else if (ex instanceof TypeMismatchException) { // 執行到了這裏 return handleTypeMismatch((TypeMismatchException) ex, request, response, handler); } // 刪除部分else if instanceof 判斷 else if (ex instanceof BindException) { return handleBindException((BindException) ex, request, response, handler); } } catch (Exception handlerException) { } return null; }
這個方法,對異常類型進行判斷,上面提到,因爲咱們傳遞的錯誤參數致使了TypeMismatchException異常,因此,根據上面的代碼,咱們本次的錯誤被handleTypeMismatch()方法處理了。handleTypeMismatch方法的代碼很是的簡單,所有代碼以下:
protected ModelAndView handleTypeMismatch(TypeMismatchException ex, HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { response.sendError(HttpServletResponse.SC_BAD_REQUEST); return new ModelAndView(); }
執行到這裏,最終返回了一個 new ModelAndView()對象。根據 [ 代碼示例-6 ] 中的代碼所示,程序終於能夠跳出這個for循環了。進入下面的if語句以後,因爲獲得的是一個空的 ModelAndView對象,因此執行了exMv.isEmpty()的代碼,return 了null。
接下來程序便回到了processDispatchResult方法,調用了mappedHandler.triggerAfterCompletion(request, response, null);以後,一切便結束了。這裏的方法調用是責任鏈設計模式,本篇不在過多的解釋,意思就是異常處理以後,繼續交給後續的intercepter處理。最終,咱們便看到了開篇所給出的400頁面。
通過上面的分析,咱們已經知道了這個400錯誤是如何發生的。那麼改如何解決呢?一般狀況下,咱們的應用都會有不少controller和方法,這麼多的controller和方法咱們不可能一個個的去處理。因此,一般來講,定義一個全局的處理器會是一個比較好的選擇。spring給了咱們不少的選擇。(感興趣的能夠看:https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc)
本例中,我爲了處理這個400錯誤,使用了以下的方式。新建一個類GlobalDefaultExceptionHandler,並保證該類能夠被spring容器初始化,其代碼以下:
@ControllerAdvice public class GlobalDefaultExceptionHandler { @ExceptionHandler(value = TypeMismatchException.class) @ResponseBody public Object defaultErrorHandler1(HttpServletRequest req, Exception e) throws Exception { if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) { throw e; } ResaultBean res = new ResaultBean("請求的參數中有格式錯誤"); return res; } @ExceptionHandler(value = HttpRequestMethodNotSupportedException.class) public Object defaultErrorHandler2(HttpServletRequest req, Exception e) throws Exception { if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) { throw e; } ModelAndView mav = new ModelAndView(); mav.addObject("exception", e); mav.addObject("url", req.getRequestURL()); mav.setViewName("error"); return mav; } }
本例中將@ ControllerAdvice 和 @ ExceptionHandler搭配使用,實現了對TypeMismatchException和HttpRequestMethodNotSupportedException的處理。當有這兩個異常發生時,分別會執行這裏的邏輯,並返回咱們自定義的結果。
注意,在defaultErrorHandler1()方法中,咱們還搭配了@ ResponseBody註解,使用過springmvc的同窗都知道,到咱們在controller的某個方法上註解@ ResponseBody的時候,表示這個方法返回的是json,而不是某個視圖頁面。同理,這裏的異常處理加上@ ResponseBody註解,表示對這個異常的處理結果返回的也是。開發api的同窗須要正是這個配置,而不是在「正常狀況下返回json,錯誤的狀況下400頁面html」那就很糟糕了。另外@ ExceptionHandler 搭配 ResponseBody使用好像是在spring 3.1以後才支持的,以前是隻能返回ModelAndView 和String ( 也是一個頁面配置)。可是這個能夠忽略,由於如今你們用的都是高版本的了。
defaultErrorHandler2()中,返回的是ModelAndView。即,咱們能夠也能夠指定返回某個頁面。在這個例子中,我使用了兩個@ExceptionHandler註解分別處理了兩個異常狀況。你固然可使用@ExceptionHandler(value = Exception.class)來處理全部的異常了。
@ExceptionHandler的原理其實就是,就是將其所註解的處理類,配置到了ExceptionHandlerExceptionResolver類的exceptionHandlerCache中,上面說的for循環在挑選處理器的時候,會找到ExceptionHandlerExceptionResolver來處理。後面就映射到了咱們自定義處理類GlobalDefaultExceptionHandler中的相應方法。而後咱們看到的結果就是:
{ "code": 10001, "message": "請求的參數中有格式錯誤" }
至此,400錯誤的發生和解決算是粗略的講完了。這裏我雖然是調試了代碼,並分析了相關的執行流程,以及設計模式。可是仍是感受略知一二。要想徹底弄清楚,仍是須要繼續深刻的。Spring真的強大的,設計的好,功能全,代碼寫的也漂亮。值得學習啊。
附加一點:
本文主要是想經過源碼來分析400錯誤發生的過程,順帶的瞭解一下SpringMVC異常處理方面的設計。這裏補充一點,若是咱們想處理請求過程當中發生的異常。那麼咱們只須要實現HandlerExceptionResolver接口便可。實現的方法以下:
public class ApiHandlerExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception) { ModelAndView model = new ModelAndView(); // do something ... return model; } }
經過這個ApiHandlerExceptionResolver,當咱們的controller方法在執行過程當中,拋出了異常(本身並未try,catch捕獲的)好比說空指針異常,數組越界異常等。就能夠走這裏了,而不是返回一個tomcat 500錯誤頁面。這個配置算是比較經常使用的,因此再也不解釋。反而是上面所說的400處理,即請求處理以前的錯誤一些應用中並未配置。