SpringMVC除了对请求URL的路由处理特别方便外,还支持对异常的统一处理机制,能够对业务操做时抛出的异常,unchecked异常以及状态码的异常进行统一处理。SpringMVC既提供简单的配置类,也提供了细粒度的异常控制机制。java
SpringMVC中全部的异常处理经过接口HandlerExceptionResolver来实现,接口中只定义了一个方法web
public interface HandlerExceptionResolver { ModelAndView resolveException( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex); }
方法中接受request和response信息,以及当前的处理Handler,和抛出的异常对象。而且提供抽象类AbstractHandlerExceptionResolver,实现resolveException方法,支持前置判断和处理,将实际处理抽象出doResolveException方法由子类来实现。ajax
SimpleMappingExceptionResolver是SpringMVC提供的一个很是便捷的简易异常处理方式,在XML中进行配置便可使用。spring
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> <!-- 默认异常视图 --> <property name="defaultErrorView" value="error"/> <!-- 视图中获取exception信息变量名 --> <property name="exceptionAttribute" value="ex"></property> <!-- 异常同视图映射关系 --> <property name="exceptionMappings"> <props> <prop key="com.lcifn.springmvc.exception.BusinessException">businessEx</prop> </props> </property> </bean>
这是极简的一种配置,exceptionMappings配置的是异常同视图之间的映射关系,它是一个Properties对象,key-value分别是异常的类路径和视图名称。defaultErrorView表示默认异常视图,若是抛出的异常没有匹配到任何视图,即会走默认异常视图。exceptionAttribute表示在视图中获取exception信息变量名,默认为exception。还有一些其余配置能够查看SimpleMappingExceptionResolver的源码来使用。mvc
SpringMVC提供了一种注解方式来灵活地配置异常处理,@ExceptionHandler中能够配置要处理的异常类型,而后定义在处理此种异常的方法上,方法只要写在Controller中,便可对Controller中全部请求方法有效。app
咱们定义一个BaseController,而且将须要处理的异常经过@ExceptionHandler定义好处理方法,这样业务Controller只须要继承这个基类就能够了。处理方法中支持Request/Response/Sessioin等相关的参数绑定。async
[@Controller](https://my.oschina.net/u/1774615) public class BaseController { @ExceptionHandler(RuntimeException.class) public ModelAndView handleRuntimeException(HttpServletRequest req, HttpServletResponse resp, RuntimeException ex){ return new ModelAndView("error"); } }
可是继承的方式仍是对业务代码形成侵入,Spring很是重要的特性就是非侵入性,于是SpringMVC提供了@ControllerAdvice,简单来讲就是Controller的切面,支持对可选择的Controller进行统一配置,用于异常处理简直再合适不过了,咱们只须要将BaseController稍稍改一下。ide
@ControllerAdvice public class AdviceController { @ExceptionHandler(RuntimeException.class) public ModelAndView handleRuntimeException(HttpServletRequest req, HttpServletResponse resp, RuntimeException ex){ return new ModelAndView("error"); } }
只须要在统一配置类上加上@ControllerAdvice注解,支持包路径,注解等过滤方式,便可完成对全部业务Controller进行控制,而业务Controller不用作anything。函数
若是请求为ajax方式,须要其余格式返回异常,在方法上加上@ResponseBody便可。this
上面介绍了经常使用的两种异常处理的配置方式,所谓知其然要知其因此然,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。
来看SimpleMappingExceptionResolver的实现:
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // Expose ModelAndView for chosen error view. // 根据request和异常对象获取异常视图名称 String viewName = determineViewName(ex, request); if (viewName != null) { // Apply HTTP status code for error views, if specified. // Only apply it if we're processing a top-level request. Integer statusCode = determineStatusCode(request, viewName); if (statusCode != null) { applyStatusCodeIfPossible(request, response, statusCode); } // 组装异常视图模型ModelAndView return getModelAndView(viewName, ex, request); } else { return null; } }
determineViewName方法决定异常视图名称,getModelAndView方法返回ModelAndView对象
protected String determineViewName(Exception ex, HttpServletRequest request) { String viewName = null; if (this.excludedExceptions != null) { for (Class<?> excludedEx : this.excludedExceptions) { if (excludedEx.equals(ex.getClass())) { return null; } } } // Check for specific exception mappings. if (this.exceptionMappings != null) { viewName = findMatchingViewName(this.exceptionMappings, ex); } // Return default error view else, if defined. if (viewName == null && this.defaultErrorView != null) { if (logger.isDebugEnabled()) { logger.debug("Resolving to default view '" + this.defaultErrorView + "' for exception of type [" + ex.getClass().getName() + "]"); } viewName = this.defaultErrorView; } return viewName; }
在determineViewName方法中,咱们配置的defaultErrorView和exceptionMappings都起了做用。更细节的就不深刻了,有兴趣能够本身去看。
ExceptionHandlerExceptionResolver支持了@ExceptionHandler注解的实现。它的抽象基类AbstractHandlerMethodExceptionResolver继承了AbstractHandlerExceptionResolver,doResolveException方法实际调用ExceptionHandlerExceptionResolver的doResolveHandlerMethodException方法。
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; } }
咱们主要关注的是如何匹配到异常处理方法的
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 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; }
匹配异常处理方法的来源有两个,一个是当前Controller,一个是全部@ControllerAdvice类。能够看到两种方式都使用了cache的方式,那么ExceptionHandlerMethod的信息怎么初始化的呢?
对每一个请求HandlerMethod的Controller类型,都实例化一个ExceptionHandlerMethodResolver来处理异常。ExceptionHandlerMethodResolver的构造函数中初始化了当前Controller中的异常处理配置。
public ExceptionHandlerMethodResolver(Class<?> handlerType) { for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) { // detectExceptionMappings方法执行探查 for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) { addExceptionMapping(exceptionType, method); } } } private List<Class<? extends Throwable>> detectExceptionMappings(Method method) { List<Class<? extends Throwable>> result = new ArrayList<Class<? extends Throwable>>(); // 探查全部ExceptionHandler注解的方法 detectAnnotationExceptionMappings(method, result); if (result.isEmpty()) { for (Class<?> paramType : method.getParameterTypes()) { if (Throwable.class.isAssignableFrom(paramType)) { result.add((Class<? extends Throwable>) paramType); } } } if (result.isEmpty()) { throw new IllegalStateException("No exception types mapped to " + method); } return result; } protected void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) { ExceptionHandler ann = AnnotationUtils.findAnnotation(method, ExceptionHandler.class); result.addAll(Arrays.asList(ann.value())); }
对@ControllerAdvice统一切面类的处理,则是在ExceptionHandlerExceptionResolver的初始化方法afterPropertiesSet中进行处理。
public void afterPropertiesSet() { // Do this first, it may add ResponseBodyAdvice beans // 初始化@ControllerAdvice中的@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); } }
initExceptionHandlerAdviceCache方法遍历上下文中全部有@ControllerAdvice注解的Bean,而后实例化成ExceptionHandlerMethodResolver类,在构造函数中初始化全部@ExceptionHandler。
private void initExceptionHandlerAdviceCache() { if (getApplicationContext() == null) { return; } if (logger.isDebugEnabled()) { logger.debug("Looking for exception mappings: " + getApplicationContext()); } // 查询全部@ControllerAdvice的Bean List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); AnnotationAwareOrderComparator.sort(adviceBeans); // 遍历,实例化ExceptionHandlerMethodResolver for (ControllerAdviceBean adviceBean : adviceBeans) { ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType()); if (resolver.hasExceptionMappings()) { 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); } } } }
匹配到exceptionHandlerMethod后,设置一些方法执行的环境,而后调用ServletInvocableHandlerMethod中的invokeAndHandle去执行,这个调用过程和正常请求的调用就是一致了。这里也不向下扩展了,能够参看SpringMVC源码(四)-请求处理。
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { // 执行请求方法 Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); setResponseStatus(webRequest); if (returnValue == null) { if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) { mavContainer.setRequestHandled(true); return; } } else if (StringUtils.hasText(getResponseStatusReason())) { mavContainer.setRequestHandled(true); return; } mavContainer.setRequestHandled(false); try { this.returnValueHandlers.handleReturnValue( returnValue, getReturnValueType(returnValue), mavContainer, webRequest); } catch (Exception ex) { if (logger.isTraceEnabled()) { logger.trace(getReturnValueHandlingErrorMessage("Error handling return value", returnValue), ex); } throw ex; } }
至此ExceptionHandlerExceptionResolver的异常处理已经基本完成。SpringMVC还内置了ResponseStatusExceptionResolver和DefaultHandlerExceptionResolver来对状态码异常和常见的请求响应异常进行统一处理。
某些状况下,SpringMVC的处理并无异常出现,但在最终的视图输出时找不到视图文件,就会显示404错误页面,很是影响用户体验。咱们能够在web.xml中对未捕获的异常以及此种4xx或5xx的异常经过<error-page>进行处理。
<error-page> <error-code>404</error-code> <location>/404</location> </error-page> <error-page> <exception-type>java.lang.Throwable</exception-type> <location>/500</location> </error-page>
一般对于系统中的异常,业务相关的尽可能自定义异常处理方式,而一些系统异常经过统一错误页面进行处理。