詳解SpringMVC中的異常處理機(jī)制
開(kāi)頭
試想一下我們一般怎么統(tǒng)一處理異常呢,答:切面。但拋開(kāi)切面不講,如果對(duì)每一個(gè)controller方法拋出的異常做專(zhuān)門(mén)處理,那么著實(shí)太費(fèi)勁了,有沒(méi)有更好的方法呢?當(dāng)然有,就是本篇文章接下來(lái)要介紹的springmvc的異常處理機(jī)制,用到了ControllerAdvice和ExceptionHandler注解,有點(diǎn)切面的感覺(jué)哈哈。
1.ExceptionHandlerExceptionResolver
首先從springmvc的異常處理解析器開(kāi)始講,當(dāng)執(zhí)行完controller方法后,不管有沒(méi)有異常產(chǎn)生都會(huì)調(diào)用DispatcherServlet#doDispatch()方法中的processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); 方法,接著會(huì)判斷是否有異常,若無(wú)異常則走正常流程,若有異常則需要進(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) {
// 無(wú)視圖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,下面將著重介紹它,先來(lái)看看這個(gè)類(lèi)的繼承結(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注解的方法,獲取方法注解帶有的異常類(lèi)型,將異常類(lèi)型和方法放入到mappedMethods中供后面獲取,獲取的時(shí)候若對(duì)應(yīng)處理此異常類(lèi)型的method有多個(gè),則需要進(jìn)行排序,選取一個(gè)異常類(lèi)型與method ExceptionHandler注解異常類(lèi)型最近的一個(gè)(深度最小的那個(gè)也即是繼承關(guān)系最少的那個(gè))具體代碼如下:
ExceptionHandlerMethodResolver
@Override
@Nullable
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
// exception為controller方法拋出的異常
// 根據(jù)異常及其類(lèi)型從上述的mappedMethods中獲取對(duì)應(yīng)的方法,再獲取方法所在的對(duì)象 封裝成ServletInvocableHandlerMethod
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
if (exceptionHandlerMethod == null) {
return null;
}
// 設(shè)置參數(shù)解析器,主要用來(lái)獲取方法的參數(shù)值的,供后續(xù)反射調(diào)用方法
if (this.argumentResolvers != null) {
exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
// 設(shè)置返回值解析器,當(dāng)執(zhí)行完方法后獲取返回值,對(duì)返回值進(jìn)行處理 或返回視圖或?qū)⒔Y(jié)果寫(xiě)入到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è)置的,將返回值寫(xiě)入到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或其他視圖類(lèi)型,則還需要借助視圖解析器如InternalResourceViewResolver對(duì)視圖進(jìn)行解析渲染,若為其他類(lèi)型的值則將值寫(xiě)入到response響應(yīng)中。
2. demo
Controller類(lèi)方法:
@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異常處理類(lèi)
@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類(lèi)中定義了兩個(gè)異常處理方法,一個(gè)處理Exception異常,一個(gè)處理RuntimeException異常,那個(gè)根據(jù)controller方法拋出的異常RuntimeException再結(jié)合上面的分析(RuntimeException到RuntimeException深度為0,RuntimeException到Exception中間繼承了一次深度為1)可以得出拋出異常類(lèi)型的處理方法為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)限控制的示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02
基于Java實(shí)現(xiàn)音樂(lè)播放器的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何利用Java編寫(xiě)一個(gè)簡(jiǎn)單的音樂(lè)播放器,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的小伙伴可以了解一下2023-07-07
Mybatis實(shí)體類(lèi)對(duì)象入?yún)⒉樵兊墓P記
這篇文章主要介紹了Mybatis實(shí)體類(lèi)對(duì)象入?yún)⒉樵兊墓P記,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06
Java中new關(guān)鍵字和newInstance方法的區(qū)別分享
在初始化一個(gè)類(lèi),生成一個(gè)實(shí)例的時(shí)候,newInstance()方法和new關(guān)鍵字除了一個(gè)是方法一個(gè)是關(guān)鍵字外,最主要的區(qū)別是創(chuàng)建對(duì)象的方式不同2013-07-07
postman中POST請(qǐng)求時(shí)參數(shù)包含參數(shù)list設(shè)置方式
這篇文章主要介紹了postman中POST請(qǐng)求時(shí)參數(shù)包含參數(shù)list設(shè)置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-05-05
Java中的時(shí)間日期API知識(shí)點(diǎn)總結(jié)
本文給大家總結(jié)了Java中的時(shí)間日期API知識(shí)點(diǎn)以及相關(guān)的實(shí)例代碼分享,有興趣的朋友參考學(xué)習(xí)下。2018-04-04
Java四舍五入時(shí)保留指定小數(shù)位數(shù)的五種方式
這篇文章主要介紹了Java四舍五入時(shí)保留指定小數(shù)位數(shù)的五種方式,幫助大家更好的理解和使用Java,感興趣的朋友可以了解下2020-09-09

