SpringMVC源碼分析-400異常處理流程及解決方法
本文設計SpringMVC異常處理體系源碼分析,SpringMVC異常處理相關類的設計模式,實際工作中異常處理的實踐。
問題場景
假設我們的SpringMVC應用中有如下控制器:
代碼示例-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是怎麽處理這個錯誤的,流程是怎樣?如果了解這些,對於我們解決問題更有幫助。
源碼調試分析
為了追蹤處理過程,我會使用斷點調試的方式。我們知道,SpringMVC的核心是DispatchServlet。所有的請求會被DispatchServlet接收,並在其doDispatch(...)方法中處理。doDispatch()方法會找到對應的handler,然後invoke。所以我們在doDispatch方法中打個斷點。我們使用postman發起一個請求,並傳遞一個錯誤的參數。先貼一點doDispatch()方法的代碼:
代碼示例-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;
異常的詳細信息是:
代碼示例-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方法代碼如下:
代碼示例-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列表有:
- ExceptionHandlerExceptionResolver
- ResponseStatusExceptionResolver
- DefaultHandlerExceptionResolver
- 自定義的ExceptionResolver 1
- ...
- 自定義的ExceptionResolver N
異常體系的設計模式
在上面的代碼中,通過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執行“三步走”,看上面代碼,這三步分別是:
- logException(ex, request);
- prepareResponse(ex, response);
- doResolveException(request, response, handler, ex);
這裏的第三部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錯誤
經過上面的分析,我們已經知道了這個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處理,即請求處理之前的錯誤一些應用中並未配置。
SpringMVC源碼分析-400異常處理流程及解決方法