SpringMVC除了對請求URL的路由處理特別方便外,還支援對異常的統一處理機制,可以對業務操作時丟擲的異常,unchecked異常以及狀態碼的異常進行統一處理。SpringMVC既提供簡單的配置類,也提供了細粒度的異常控制機制。
SpringMVC中所有的異常處理通過介面HandlerExceptionResolver來實現,介面中只定義了一個方法
public interface HandlerExceptionResolver { ModelAndView resolveException( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex); }
方法中接受request和response資訊,以及當前的處理Handler,和丟擲的異常物件。並且提供抽象類AbstractHandlerExceptionResolver,實現resolveException方法,支援前置判斷和處理,將實際處理抽象出doResolveException方法由子類來實現。
@ControllerAdvice和@ExceptionHandler的簡單使用
@ControllerAdvice public class ExceptionAdvice { @ExceptionHandler({ArrayIndexOutOfBoundsException.class}) @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ResponseDTO handleArrayIndexOutOfBoundsException(ArrayIndexOutOfBoundsException e) { // TODO 記錄log日誌 e.printStackTrace(); ResponseDTO responseDTO = new ResponseDTO(); responseDTO.wrapResponse(ServiceCodeEnum.E999997, "陣列越界異常"); return responseDTO; } @ExceptionHandler(value = ParamException.class) @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ResponseDTO handleParamException(ParamException e) { // TODO 記錄log日誌 e.printStackTrace(); ResponseDTO responseDTO = new ResponseDTO(); responseDTO.wrapResponse(ServiceCodeEnum.E999998, "輸入引數錯誤"); return responseDTO; } @ExceptionHandler({Exception.class}) @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ResponseDTO handleException(Exception e) { // TODO 記錄log日誌 e.printStackTrace(); ResponseDTO responseDTO = new ResponseDTO(); responseDTO.wrapResponse(ServiceCodeEnum.E999999, "未知異常"); return responseDTO; } }
我們看看 @ControllerAdvice
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface ControllerAdvice { @AliasFor("basePackages") String[] value() default {}; @AliasFor("value") String[] basePackages() default {}; Class<?>[] basePackageClasses() default {}; Class<?>[] assignableTypes() default {}; Class<? extends Annotation>[] annotations() default {}; }
ControllerAdvice 被 @Component 修飾,則說明標記 @ControllerAdvice 會被掃描到容器中
Spring mvc 的配置如下(這裡用到了mvc:annotation-driven):
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cxf="http://cxf.apache.org/core" xmlns:p="http://cxf.apache.org/policy" xmlns:ss="http://www.springframework.org/schema/security" xmlns:jaxws="http://cxf.apache.org/jaxws" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd http://cxf.apache.org/policy http://cxf.apache.org/schemas/policy.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd http://cxf.apache.org/bindings/soap http://cxf.apache.org/schemas/configuration/soap.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <context:component-scan base-package="frame.web.controller;frame.web.advice" /> <!--===================== view resovler ===================== --> <bean id="jstlViewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver"> <property name="order" value="1" /> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" /> <property name="prefix" value="/WEB-INF/jsp/" /> </bean> <mvc:annotation-driven/> <!-- 自定義引數轉換 --> <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> </bean> </beans>
package org.springframework.web.servlet.config; class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { /** *parse是這個類的核心方法,它用於解析 annotation-drive標籤裡的內容,根據標籤裡的內容往spring ioc容器裡注入具體的物件。 **/ @Override public BeanDefinition parse(Element element, ParserContext parserContext) { Object source = parserContext.extractSource(element); XmlReaderContext readerContext = parserContext.getReaderContext(); CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(element.getTagName(), source); parserContext.pushContainingComponent(compDefinition); RuntimeBeanReference contentNegotiationManager = getContentNegotiationManager(element, source, parserContext); //這裡有我們熟悉的RequestMappingHandlerMapping, RootBeanDefinition handlerMappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class); handlerMappingDef.setSource(source); handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); handlerMappingDef.getPropertyValues().add("order", 0); handlerMappingDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager); if (element.hasAttribute("enable-matrix-variables")) { Boolean enableMatrixVariables = Boolean.valueOf(element.getAttribute("enable-matrix-variables")); handlerMappingDef.getPropertyValues().add("removeSemicolonContent", !enableMatrixVariables); } else if (element.hasAttribute("enableMatrixVariables")) { Boolean enableMatrixVariables = Boolean.valueOf(element.getAttribute("enableMatrixVariables")); handlerMappingDef.getPropertyValues().add("removeSemicolonContent", !enableMatrixVariables); } configurePathMatchingProperties(handlerMappingDef, element, parserContext); readerContext.getRegistry().registerBeanDefinition(HANDLER_MAPPING_BEAN_NAME , handlerMappingDef); RuntimeBeanReference corsConfigurationsRef = MvcNamespaceUtils.registerCorsConfigurations(null, parserContext, source); handlerMappingDef.getPropertyValues().add("corsConfigurations", corsConfigurationsRef); //這裡會注入具體的ConversionService用於將json,xml轉成Spring mvc裡的請求和返回物件 RuntimeBeanReference conversionService = getConversionService(element, source, parserContext); RuntimeBeanReference validator = getValidator(element, source, parserContext); RuntimeBeanReference messageCodesResolver = getMessageCodesResolver(element); RootBeanDefinition bindingDef = new RootBeanDefinition(ConfigurableWebBindingInitializer.class); bindingDef.setSource(source); bindingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); bindingDef.getPropertyValues().add("conversionService", conversionService); bindingDef.getPropertyValues().add("validator", validator); bindingDef.getPropertyValues().add("messageCodesResolver", messageCodesResolver); ManagedList<?> messageConverters = getMessageConverters(element, source, parserContext); ManagedList<?> argumentResolvers = getArgumentResolvers(element, parserContext); ManagedList<?> returnValueHandlers = getReturnValueHandlers(element, parserContext); String asyncTimeout = getAsyncTimeout(element); RuntimeBeanReference asyncExecutor = getAsyncExecutor(element); ManagedList<?> callableInterceptors = getCallableInterceptors(element, source, parserContext); ManagedList<?> deferredResultInterceptors = getDeferredResultInterceptors(element, source, parserContext); //RequestMappingHandlerAdapter也會在這裡注入 RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); handlerAdapterDef.setSource(source); handlerAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); handlerAdapterDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager); handlerAdapterDef.getPropertyValues().add("webBindingInitializer", bindingDef); handlerAdapterDef.getPropertyValues().add("messageConverters", messageConverters); addRequestBodyAdvice(handlerAdapterDef); addResponseBodyAdvice(handlerAdapterDef); if (element.hasAttribute("ignore-default-model-on-redirect")) { Boolean ignoreDefaultModel = Boolean.valueOf(element.getAttribute("ignore-default-model-on-redirect")); handlerAdapterDef.getPropertyValues().add("ignoreDefaultModelOnRedirect", ignoreDefaultModel); } else if (element.hasAttribute("ignoreDefaultModelOnRedirect")) { // "ignoreDefaultModelOnRedirect" spelling is deprecated Boolean ignoreDefaultModel = Boolean.valueOf(element.getAttribute("ignoreDefaultModelOnRedirect")); handlerAdapterDef.getPropertyValues().add("ignoreDefaultModelOnRedirect", ignoreDefaultModel); } if (argumentResolvers != null) { handlerAdapterDef.getPropertyValues().add("customArgumentResolvers", argumentResolvers); } if (returnValueHandlers != null) { handlerAdapterDef.getPropertyValues().add("customReturnValueHandlers", returnValueHandlers); } if (asyncTimeout != null) { handlerAdapterDef.getPropertyValues().add("asyncRequestTimeout", asyncTimeout); } if (asyncExecutor != null) { handlerAdapterDef.getPropertyValues().add("taskExecutor", asyncExecutor); } handlerAdapterDef.getPropertyValues().add("callableInterceptors", callableInterceptors); handlerAdapterDef.getPropertyValues().add("deferredResultInterceptors", deferredResultInterceptors); readerContext.getRegistry().registerBeanDefinition(HANDLER_ADAPTER_BEAN_NAME , handlerAdapterDef); String uriCompContribName = MvcUriComponentsBuilder.MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME; RootBeanDefinition uriCompContribDef = new RootBeanDefinition(CompositeUriComponentsContributorFactoryBean.class); uriCompContribDef.setSource(source); uriCompContribDef.getPropertyValues().addPropertyValue("handlerAdapter", handlerAdapterDef); uriCompContribDef.getPropertyValues().addPropertyValue("conversionService", conversionService); readerContext.getRegistry().registerBeanDefinition(uriCompContribName, uriCompContribDef); RootBeanDefinition csInterceptorDef = new RootBeanDefinition(ConversionServiceExposingInterceptor.class); csInterceptorDef.setSource(source); csInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(0, conversionService); RootBeanDefinition mappedCsInterceptorDef = new RootBeanDefinition(MappedInterceptor.class); mappedCsInterceptorDef.setSource(source); mappedCsInterceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); mappedCsInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(0, (Object) null); mappedCsInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(1, csInterceptorDef); String mappedInterceptorName = readerContext.registerWithGeneratedName(mappedCsInterceptorDef); //這裡有我們需要找的ExceptionHandlerExceptionResolver, RootBeanDefinition exceptionHandlerExceptionResolver = new RootBeanDefinition(ExceptionHandlerExceptionResolver.class); exceptionHandlerExceptionResolver.setSource(source); exceptionHandlerExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); exceptionHandlerExceptionResolver.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager); exceptionHandlerExceptionResolver.getPropertyValues().add("messageConverters", messageConverters); exceptionHandlerExceptionResolver.getPropertyValues().add("order", 0); addResponseBodyAdvice(exceptionHandlerExceptionResolver); if (argumentResolvers != null) { exceptionHandlerExceptionResolver.getPropertyValues().add("customArgumentResolvers", argumentResolvers); } if (returnValueHandlers != null) { exceptionHandlerExceptionResolver.getPropertyValues().add("customReturnValueHandlers", returnValueHandlers); } String methodExceptionResolverName = readerContext.registerWithGeneratedName(exceptionHandlerExceptionResolver); RootBeanDefinition responseStatusExceptionResolver = new RootBeanDefinition(ResponseStatusExceptionResolver.class); responseStatusExceptionResolver.setSource(source); responseStatusExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); responseStatusExceptionResolver.getPropertyValues().add("order", 1); String responseStatusExceptionResolverName = readerContext.registerWithGeneratedName(responseStatusExceptionResolver); RootBeanDefinition defaultExceptionResolver = new RootBeanDefinition(DefaultHandlerExceptionResolver.class); defaultExceptionResolver.setSource(source); defaultExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); defaultExceptionResolver.getPropertyValues().add("order", 2); String defaultExceptionResolverName = readerContext.registerWithGeneratedName(defaultExceptionResolver); parserContext.registerComponent(new BeanComponentDefinition(handlerMappingDef, HANDLER_MAPPING_BEAN_NAME)); parserContext.registerComponent(new BeanComponentDefinition(handlerAdapterDef, HANDLER_ADAPTER_BEAN_NAME)); parserContext.registerComponent(new BeanComponentDefinition(uriCompContribDef, uriCompContribName)); parserContext.registerComponent(new BeanComponentDefinition(exceptionHandlerExceptionResolver, methodExceptionResolverName)); parserContext.registerComponent(new BeanComponentDefinition(responseStatusExceptionResolver, responseStatusExceptionResolverName)); parserContext.registerComponent(new BeanComponentDefinition(defaultExceptionResolver, defaultExceptionResolverName)); parserContext.registerComponent(new BeanComponentDefinition(mappedCsInterceptorDef, mappedInterceptorName)); // Ensure BeanNameUrlHandlerMapping (SPR-8289) and default HandlerAdapters are not "turned off" MvcNamespaceUtils.registerDefaultComponents(parserContext, source); parserContext.popAndRegisterContainingComponent(); return null; } }
package org.springframework.web.servlet.mvc.method.annotation; //我們考到這個類實現了InitializingBean,則容器初始化的時候在例項化此Bean後會呼叫afterPropertiesSet() public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver implements ApplicationContextAware, InitializingBean { //這裡有個map用於儲存ControllerAdviceBean和ExceptionHandlerMethodResolver private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache = new LinkedHashMap<ControllerAdviceBean, ExceptionHandlerMethodResolver>(); //這個方法是由spring 容器呼叫的 @Override public void afterPropertiesSet() { // Do this first, it may add ResponseBodyAdvice beans //這個方法裡會處理ExceptionHandler initExceptionHandlerAdviceCache(); if (this.argumentResolvers == null) { List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers(); this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); } if (this.returnValueHandlers == null) { List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers(); this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); } } /** *這個方法裡會在spring ioc容器裡找出標註了@ControllerAdvice的類,如果有方法標註了@ExceptionHandler會生成一個ExceptionHandlerMethodResolver類用於處理異常並放到exceptionHandlerAdviceCache這個map快取類裡。 **/ private void initExceptionHandlerAdviceCache() { if (getApplicationContext() == null) { return; } if (logger.isDebugEnabled()) { logger.debug("Looking for exception mappings: " + getApplicationContext()); } //這裡會找到容器裡標註了@ControllerAdvice註解的類 List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); AnnotationAwareOrderComparator.sort(adviceBeans); for (ControllerAdviceBean adviceBean : adviceBeans) { //這個構造方法裡會檢查ControllerAdvice類裡是否有@ExceptionHandler標註的方法,在ExceptionHandlerMethodResolver 有個異常的map。 //在ExceptionHandlerMethodResolver構造器中會通過反射拿到所有標註@ExceptionHandler的方法並加入ExceptionHandlerMethodResolver的map中 //key為 @ExceptionHandler(value = ParamException.class) 標註的value,這裡就是ParamException.class,值為標註@ExceptionHandler的Method ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType()); if (resolver.hasExceptionMappings()) { //如果有@ExceptionHandler方法,會執行下面的邏輯 //將標註@ControllerAdvice的類Bean,和此Bean中封裝了所有Exception為key,Method為value的Map的ExceptionHandlerMethodResolver物件加入到exceptionHandlerAdviceCache的快取中 this.exceptionHandlerAdviceCache.put(adviceBean, resolver); if (logger.isInfoEnabled()) { logger.info("Detected @ExceptionHandler methods in " + adviceBean); } } if (ResponseBodyAdvice.class.isAssignableFrom(adviceBean.getBeanType())) { this.responseBodyAdvice.add(adviceBean); if (logger.isInfoEnabled()) { logger.info("Detected ResponseBodyAdvice implementation in " + adviceBean); } } } } /** ** 這個方法會根據exceptionHandlerAdviceCache這個找到具體需要處理異常的方法,這個後面再講 */ protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) { Class<?> handlerType = (handlerMethod != null ? handlerMethod.getBeanType() : null); if (handlerMethod != null) { ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType); if (resolver == null) { resolver = new ExceptionHandlerMethodResolver(handlerType); this.exceptionHandlerCache.put(handlerType, resolver); } Method method = resolver.resolveMethod(exception); if (method != null) { return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method); } } for (Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) { if (entry.getKey().isApplicableToBeanType(handlerType)) { ExceptionHandlerMethodResolver resolver = entry.getValue(); //根據具體的異常找到處理異常的方法,然後呼叫 Method method = resolver.resolveMethod(exception); if (method != null) { return new ServletInvocableHandlerMethod(entry.getKey().resolveBean(), method); } } } return null; } }
我們來看看 ExceptionHandlerMethodResolver這個類
public class ExceptionHandlerMethodResolver { public static final MethodFilter EXCEPTION_HANDLER_METHODS = new MethodFilter() { public boolean matches(Method method) { return AnnotationUtils.findAnnotation(method, ExceptionHandler.class) != null; } }; private static final Method NO_METHOD_FOUND = ClassUtils.getMethodIfAvailable(System.class, "currentTimeMillis", new Class[0]); //此快取Map存放了@ControllerAdvice中所有註解了@ExceptionHandler的方法,其中@ExceptionHandler的value也就是Exception做為Key,值為當前Method private final Map<Class<? extends Throwable>, Method> mappedMethods = new ConcurrentHashMap(16); private final Map<Class<? extends Throwable>, Method> exceptionLookupCache = new ConcurrentHashMap(16); public ExceptionHandlerMethodResolver(Class<?> handlerType) { //通過反射拿到當前Class的所有方法,也就是標註了@ControllerAdvice的所有方法 Iterator var2 = MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS).iterator(); //遍歷所有的方法,尋找標註了@ExceptionHandler的方法 while(var2.hasNext()) { Method method = (Method)var2.next(); //這裡獲取到標註了@ExceptionHandler的方法上所有的@ExceptionHandler中的value //如{ArrayIndexOutOfBoundsException.class,ParamException.calss} Iterator var4 = this.detectExceptionMappings(method).iterator(); while(var4.hasNext()) { Class<? extends Throwable> exceptionType = (Class)var4.next(); //將ArrayIndexOutOfBoundsException.class作為key,method做為value加入到map快取中 this.addExceptionMapping(exceptionType, method); } } } private List<Class<? extends Throwable>> detectExceptionMappings(Method method) { List<Class<? extends Throwable>> result = new ArrayList(); //這裡獲取到標註了@ExceptionHandler的方法上所有的@ExceptionHandler中的value //如{ArrayIndexOutOfBoundsException.class,ParamException.calss} this.detectAnnotationExceptionMappings(method, result); if (result.isEmpty()) { Class[] var3 = method.getParameterTypes(); int var4 = var3.length; for(int var5 = 0; var5 < var4; ++var5) { Class<?> paramType = var3[var5]; if (Throwable.class.isAssignableFrom(paramType)) { result.add(paramType); } } } Assert.notEmpty(result, "No exception types mapped to {" + method + "}"); return result; } protected void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) { //判斷此方法是否標記@ExceptionHandler,如果沒有則返回null,如果有標記則返回ExceptionHandler ExceptionHandler ann = (ExceptionHandler)AnnotationUtils.findAnnotation(method, ExceptionHandler.class); //獲取ExceptionHandler註解的所有值,這裡是一個陣列,有可能有多個,如@ExceptionHandler({ArrayIndexOutOfBoundsException.class,ParamException.calss}) result.addAll(Arrays.asList(ann.value())); } }
異常處理原理
SpringMVC怎麼在請求處理的過程中完成對異常的統一處理的呢?我們從原始碼來深度解讀。
回到DispatcherServlet的doDispatcher方法
try { processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // Determine handler for the current request. mappedHandler = getHandler(processedRequest); if (mappedHandler == null || mappedHandler.getHandler() == null) { noHandlerFound(processedRequest, response); return; } // Determine handler adapter for the current request. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; } catch (Throwable err) { dispatchException = new NestedServletException("Handler dispatch failed", err); } processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
可以看到對請求處理的核心處理使用一個大的try/catch,如果出現異常,統一封裝成dispatchException交給processDispatchResult方法進行處理。我們知道processDispatchResult方法用來對返回檢視進行操作,而同時也對異常進行統一處理。
在processDispatchResult中,首先對異常進行判斷。
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來操作。
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; } } // 如果異常檢視存在,則轉向異常檢視 if (exMv != null) { 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()) { exMv.setViewName(getDefaultViewName(request)); } if (logger.isDebugEnabled()) { logger.debug("Handler execution resulted in exception - forwarding to resolved error view: " + exMv, ex); } WebUtils.exposeErrorRequestAttributes(request, ex, getServletName()); return exMv; } throw ex; }
我們主要關注異常處理器對異常的處理,SpringMVC通過HandlerExceptionResolver的resolveException呼叫實現類的實際實現方法doResolveException。
推薦部落格
ExceptionHandlerExceptionResolver
ExceptionHandlerExceptionResolver支援了@ExceptionHandler註解的實現。它的抽象基類AbstractHandlerMethodExceptionResolver繼承了AbstractHandlerExceptionResolver,doResolveException方法實際呼叫ExceptionHandlerExceptionResolver的doResolveHandlerMethodException方法。
protected final ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { return this.doResolveHandlerMethodException(request, response, (HandlerMethod)handler, ex); } protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, Exception exception) { // 根據HandlerMethod和exception獲取異常處理的Method ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception); if (exceptionHandlerMethod == null) { return null; } // 設定異常處理方法的引數解析器和返回值解析器 exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); 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 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; } // 對返回的檢視模型進行處理 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()); if (!mavContainer.isViewReference()) { mav.setView((View) mavContainer.getView()); } if (model instanceof RedirectAttributes) { Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes(); request = webRequest.getNativeRequest(HttpServletRequest.class); RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes); } return mav; } }
我們主要關注的是如何匹配到異常處理方法的,也就是 ExceptionHandlerExceptionResolver中的 getExceptionHandlerMethod
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) { Class<?> handlerType = (handlerMethod != null ? handlerMethod.getBeanType() : null); // 從當前Controller中匹配異常處理Method,此處我們暫時不分析 if (handlerMethod != null) { ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType); if (resolver == null) { resolver = new ExceptionHandlerMethodResolver(handlerType); this.exceptionHandlerCache.put(handlerType, resolver); } Method method = resolver.resolveMethod(exception); if (method != null) { return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method); } } // 從ControllerAdvice中匹配異常處理Method // 我們知道容器初始化的時候,已經尋找所有標註了@ControllerAdvice的類,並將此類標註了@ExceptionHandler的方法放到當前類的exceptionHandlerAdviceCache中 //遍歷所有的@ControllerAdvice生成的快取 for (Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) { if (entry.getKey().isApplicableToBeanType(handlerType)) { //拿到ExceptionHandlerMethodResolver,這個物件裡包含了標註為@ExceptionHandler的Key為Excpthion,value為Method的快取Map ExceptionHandlerMethodResolver resolver = entry.getValue(); //尋找ExceptionHandlerMethodResolver有沒有標註@ExceptionHandler能匹配當前異常的方法 Method method = resolver.resolveMethod(exception); if (method != null) { return new ServletInvocableHandlerMethod(entry.getKey().resolveBean(), method); } } } return null; } public Method resolveMethod(Exception exception) { Method method = this.resolveMethodByExceptionType(exception.getClass()); if (method == null) { Throwable cause = exception.getCause(); if (cause != null) { method = this.resolveMethodByExceptionType(cause.getClass()); } } return method; } public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) { //先從exceptionLookupCache快取中拿,第一次肯定拿不到,因為我們是存在mappedMethods這個快取中 Method method = (Method)this.exceptionLookupCache.get(exceptionType); if (method == null) { method = this.getMappedMethod(exceptionType); this.exceptionLookupCache.put(exceptionType, method != null ? method : NO_METHOD_FOUND); } return method != NO_METHOD_FOUND ? method : null; } private Method getMappedMethod(Class<? extends Throwable> exceptionType) { List<Class<? extends Throwable>> matches = new ArrayList(); //拿到所有的Key Iterator var3 = this.mappedMethods.keySet().iterator(); while(var3.hasNext()) { Class<? extends Throwable> mappedException = (Class)var3.next(); //判斷exceptionType是不是mappedException本身或者其子類 if (mappedException.isAssignableFrom(exceptionType)) { matches.add(mappedException); } } if (!matches.isEmpty()) { Collections.sort(matches, new ExceptionDepthComparator(exceptionType)); //返回匹配到異常的Method return (Method)this.mappedMethods.get(matches.get(0)); } else { return null; } }
匹配到exceptionHandlerMethod後,設定一些方法執行的環境,然後呼叫ServletInvocableHandlerMethod中的invokeAndHandle去執行,這個呼叫過程和正常請求的呼叫就是一致了。
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, new Object[]{exception, cause, handlerMethod});
這裡就是呼叫異常處理的方法,總體來說,就是SpringMvc啟動的時候初始化異常處理的元件,將 @ControllerAdvice標記的特殊類和@ExceptionHandler 標記的方法存入快取中,當目標Controller出現異常的時候,就通過丟擲的異常在快取中找到對應的處理方法,然後去呼叫對應的異常處理方法就OK了。