詳解SpringMVC中的異常處理機(jī)制
開頭
試想一下我們一般怎么統(tǒng)一處理異常呢,答:切面。但拋開切面不講,如果對(duì)每一個(gè)controller方法拋出的異常做專門處理,那么著實(shí)太費(fèi)勁了,有沒有更好的方法呢?當(dāng)然有,就是本篇文章接下來要介紹的springmvc的異常處理機(jī)制,用到了ControllerAdvice和ExceptionHandler注解,有點(diǎn)切面的感覺哈哈。
1.ExceptionHandlerExceptionResolver
首先從springmvc的異常處理解析器開始講,當(dāng)執(zhí)行完controller方法后,不管有沒有異常產(chǎn)生都會(huì)調(diào)用DispatcherServlet#doDispatch()
方法中的processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
方法,接著會(huì)判斷是否有異常,若無異常則走正常流程,若有異常則需要進(jìn)行處理 mv = processHandlerException(request, response, handler, exception);
再接著就是遍歷spring已經(jīng)注冊(cè)的異常處理解析器直到有處理器返回mav
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { if (exception != null) { if (exception instanceof ModelAndViewDefiningException) { logger.debug("ModelAndViewDefiningException encountered", exception); mv = ((ModelAndViewDefiningException) exception).getModelAndView(); } else { Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); // 執(zhí)行處理器產(chǎn)生的異常處理 mv = processHandlerException(request, response, handler, exception); // 是否有異常視圖返回 errorView = (mv != null); } } // Did the handler return a view to render? 處理程序是否返回要渲染的視圖 if (mv != null && !mv.wasCleared()) { // 渲染視圖 render(mv, request, response); if (errorView) { WebUtils.clearErrorRequestAttributes(request); } } else { if (logger.isDebugEnabled()) { logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() + "': assuming HandlerAdapter completed request handling"); } } }
@Nullable protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception { // Check registered HandlerExceptionResolvers... ModelAndView exMv = null; if (this.handlerExceptionResolvers != null) { for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) { exMv = handlerExceptionResolver.resolveException(request, response, handler, ex); if (exMv != null) { break; } } } if (exMv != null) { // 無視圖view if (exMv.isEmpty()) { request.setAttribute(EXCEPTION_ATTRIBUTE, ex); return null; } // We might still need view name translation for a plain error model... if (!exMv.hasView()) { String defaultViewName = getDefaultViewName(request); if (defaultViewName != null) { exMv.setViewName(defaultViewName); } } WebUtils.exposeErrorRequestAttributes(request, ex, getServletName()); return exMv; } throw ex; }
其中最重要也是最常使用的一個(gè)處理器就是ExceptionHandlerExceptionResolver,下面將著重介紹它,先來看看這個(gè)類的繼承結(jié)構(gòu)圖,實(shí)現(xiàn)了InitializingBean接口,在這個(gè)bean創(chuàng)建完成之前會(huì)調(diào)用生命周期初始化方法afterPropertiesSet()
,這里面包含了對(duì)@ControllerAdvice
注解的解析,初始化完后的信息供后續(xù)解析異常使用。
實(shí)現(xiàn)HandlerExceptionResolver
接口,實(shí)現(xiàn)解析方法resolveException()
public interface HandlerExceptionResolver { /** * Try to resolve the given exception that got thrown during handler execution, * returning a {@link ModelAndView} that represents a specific error page if appropriate. * <p>The returned {@code ModelAndView} may be {@linkplain ModelAndView#isEmpty() empty} * to indicate that the exception has been resolved successfully but that no view * should be rendered, for instance by setting a status code. * @param request current HTTP request * @param response current HTTP response * @param handler the executed handler, or {@code null} if none chosen at the * time of the exception (for example, if multipart resolution failed) * @param ex the exception that got thrown during handler execution * @return a corresponding {@code ModelAndView} to forward to, * or {@code null} for default processing in the resolution chain */ @Nullable ModelAndView resolveException( HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex); }
@Override public void afterPropertiesSet() { // Do this first, it may add ResponseBodyAdvice beans // 初始化異常注解 @ControllerAdvice initExceptionHandlerAdviceCache(); } private void initExceptionHandlerAdviceCache() { if (getApplicationContext() == null) { return; } if (logger.isDebugEnabled()) { logger.debug("Looking for exception mappings: " + getApplicationContext()); } // 解析有@ControllerAdvice注解的bean,并將這個(gè)bean構(gòu)建成ControllerAdviceBean對(duì)象 List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); // 將ControllerAdviceBean根據(jù)order排序 AnnotationAwareOrderComparator.sort(adviceBeans); for (ControllerAdviceBean adviceBean : adviceBeans) { Class<?> beanType = adviceBean.getBeanType(); if (beanType == null) { throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean); } ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType); // mappedMethods 映射不為空 if (resolver.hasExceptionMappings()) { // 添加到緩存中 this.exceptionHandlerAdviceCache.put(adviceBean, resolver); if (logger.isInfoEnabled()) { logger.info("Detected @ExceptionHandler methods in " + adviceBean); } } // 若實(shí)現(xiàn)了ResponseBodyAdvice接口(暫不介紹) if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) { this.responseBodyAdvice.add(adviceBean); if (logger.isInfoEnabled()) { logger.info("Detected ResponseBodyAdvice implementation in " + adviceBean); } } } }
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
這行代碼會(huì)解析擁有@ControllerAdvice
注解的class,并且會(huì)遍歷class中帶有 @ExceptionHandler
注解的方法,獲取方法注解帶有的異常類型,將異常類型和方法放入到mappedMethods
中供后面獲取,獲取的時(shí)候若對(duì)應(yīng)處理此異常類型的method有多個(gè),則需要進(jìn)行排序,選取一個(gè)異常類型與method ExceptionHandler注解異常類型最近的一個(gè)(深度最小的那個(gè)也即是繼承關(guān)系最少的那個(gè))具體代碼如下:
ExceptionHandlerMethodResolver
@Override @Nullable protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) { // exception為controller方法拋出的異常 // 根據(jù)異常及其類型從上述的mappedMethods中獲取對(duì)應(yīng)的方法,再獲取方法所在的對(duì)象 封裝成ServletInvocableHandlerMethod ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception); if (exceptionHandlerMethod == null) { return null; } // 設(shè)置參數(shù)解析器,主要用來獲取方法的參數(shù)值的,供后續(xù)反射調(diào)用方法 if (this.argumentResolvers != null) { exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } // 設(shè)置返回值解析器,當(dāng)執(zhí)行完方法后獲取返回值,對(duì)返回值進(jìn)行處理 或返回視圖或?qū)⒔Y(jié)果寫入到response if (this.returnValueHandlers != null) { exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } ServletWebRequest webRequest = new ServletWebRequest(request, response); ModelAndViewContainer mavContainer = new ModelAndViewContainer(); try { if (logger.isDebugEnabled()) { logger.debug("Invoking @ExceptionHandler method: " + exceptionHandlerMethod); } Throwable cause = exception.getCause(); if (cause != null) { // Expose cause as provided argument as well // 執(zhí)行異常處理方法,也就是我們的自定義的異常處理方法 exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod); } else { // Otherwise, just the given exception as-is exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod); } } catch (Throwable invocationEx) { // Any other than the original exception is unintended here, // probably an accident (e.g. failed assertion or the like). if (invocationEx != exception && logger.isWarnEnabled()) { logger.warn("Failed to invoke @ExceptionHandler method: " + exceptionHandlerMethod, invocationEx); } // Continue with default processing of the original exception... return null; } // 根據(jù)后續(xù)的返回值解析器設(shè)置的,將返回值寫入到response中了直接返回空的mav if (mavContainer.isRequestHandled()) { return new ModelAndView(); } else { ModelMap model = mavContainer.getModel(); HttpStatus status = mavContainer.getStatus(); ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status); mav.setViewName(mavContainer.getViewName()); // (this.view instanceof String) if (!mavContainer.isViewReference()) { mav.setView((View) mavContainer.getView()); } if (model instanceof RedirectAttributes) { Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes(); RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes); } return mav; } }
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod);
此方法執(zhí)行完成后已經(jīng)完成了異常處理方法的調(diào)用,若方法返回值為視圖ModelAndView或其他視圖類型,則還需要借助視圖解析器如InternalResourceViewResolver
對(duì)視圖進(jìn)行解析渲染,若為其他類型的值則將值寫入到response響應(yīng)中。
2. demo
Controller類方法:
@Controller @RequestMapping(value = "test") public class HelloWorldController{ @Data public static class User { private String username; private Integer age; private String address; } @RequestMapping(value = "user/get", method = RequestMethod.POST) @ResponseBody public Object testObject(@RequestBody @Valid User user, @RequestParam String address) { user.setAddress(address); // 這里特意拋出RuntimeException異常 throw new RuntimeException("this is a exception"); } }
ExceptionHandlerController異常處理類
@ControllerAdvice @ResponseBody public class ExceptionHandlerController { @ExceptionHandler(value = Exception.class) public Object handleException(Exception e) { return CommonResult.fail("Exception:" + e.getMessage()); } @ExceptionHandler(value = RuntimeException.class) public Object handlerRuntimeException(Exception e) { return CommonResult.fail("handlerRuntimeException:" + e.getMessage()); } }
ExceptionHandlerController類中定義了兩個(gè)異常處理方法,一個(gè)處理Exception異常,一個(gè)處理RuntimeException異常,那個(gè)根據(jù)controller方法拋出的異常RuntimeException再結(jié)合上面的分析(RuntimeException到RuntimeException深度為0,RuntimeException到Exception中間繼承了一次深度為1)可以得出拋出異常類型的處理方法為handlerRuntimeException
方法。 運(yùn)行程序結(jié)果如下:
到此這篇關(guān)于詳解SpringMVC中的異常處理機(jī)制的文章就介紹到這了,更多相關(guān)SpringMVC異常處理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MyBatis-Plus攔截器實(shí)現(xiàn)數(shù)據(jù)權(quán)限控制的示例
本文主要介紹了MyBatis-Plus攔截器實(shí)現(xiàn)數(shù)據(jù)權(quán)限控制的示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02Mybatis實(shí)體類對(duì)象入?yún)⒉樵兊墓P記
這篇文章主要介紹了Mybatis實(shí)體類對(duì)象入?yún)⒉樵兊墓P記,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06Java中new關(guān)鍵字和newInstance方法的區(qū)別分享
在初始化一個(gè)類,生成一個(gè)實(shí)例的時(shí)候,newInstance()方法和new關(guān)鍵字除了一個(gè)是方法一個(gè)是關(guān)鍵字外,最主要的區(qū)別是創(chuàng)建對(duì)象的方式不同2013-07-07postman中POST請(qǐng)求時(shí)參數(shù)包含參數(shù)list設(shè)置方式
這篇文章主要介紹了postman中POST請(qǐng)求時(shí)參數(shù)包含參數(shù)list設(shè)置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-05-05Java中的時(shí)間日期API知識(shí)點(diǎn)總結(jié)
本文給大家總結(jié)了Java中的時(shí)間日期API知識(shí)點(diǎn)以及相關(guān)的實(shí)例代碼分享,有興趣的朋友參考學(xué)習(xí)下。2018-04-04Java四舍五入時(shí)保留指定小數(shù)位數(shù)的五種方式
這篇文章主要介紹了Java四舍五入時(shí)保留指定小數(shù)位數(shù)的五種方式,幫助大家更好的理解和使用Java,感興趣的朋友可以了解下2020-09-09