2019独角兽企业重金招聘Python工程师标准>>>
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方法由子类来实现。
1.SimpleMappingExceptionResolverSimpleMappingExceptionResolver是SpringMVC提供的一个非常便捷的简易异常处理方式,在XML中进行配置即可使用。
这是极简的一种配置,exceptionMappings配置的是异常同视图之间的映射关系,它是一个Properties对象,key-value分别是异常的类路径和视图名称。defaultErrorView表示默认异常视图,如果抛出的异常没有匹配到任何视图,即会走默认异常视图。exceptionAttribute表示在视图中获取exception信息变量名,默认为exception。还有一些其他配置可以查看SimpleMappingExceptionResolver的源码来使用。
2.@ExceptionHandlerSpringMVC提供了一种注解方式来灵活地配置异常处理,@ExceptionHandler中可以配置要处理的异常类型,然后定义在处理此种异常的方法上,方法只要写在Controller中,即可对Controller中所有请求方法有效。
我们定义一个BaseController,并且将需要处理的异常通过@ExceptionHandler定义好处理方法,这样业务Controller只需要继承这个基类就可以了。处理方法中支持Request/Response/Sessioin等相关的参数绑定。
[@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稍稍改一下。
@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即可。
3.异常处理原理上面介绍了常用的两种异常处理的配置方式,所谓知其然要知其所以然,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
来看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);}// 组装异常视图模型ModelAndViewreturn 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
ExceptionHandlerExceptionResolver支持了@ExceptionHandler注解的实现。它的抽象基类AbstractHandlerMethodExceptionResolver继承了AbstractHandlerExceptionResolver,doResolveException方法实际调用ExceptionHandlerExceptionResolver的doResolveHandlerMethodException方法。
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod, Exception exception) {// 根据HandlerMethod和exception获取异常处理的MethodServletInvocableHandlerMethod 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 wellexceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod);}else {// Otherwise, just the given exception as-isexceptionHandlerMethod.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
}
我们主要关注的是如何匹配到异常处理方法的
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {Class> handlerType = (handlerMethod != null ? handlerMethod.getBeanType() : null);// 从当前Controller中匹配异常处理Methodif (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中匹配异常处理Methodfor (Entry
}
匹配异常处理方法的来源有两个,一个是当前Controller,一个是所有@ControllerAdvice类。可以看到两种方式都使用了cache的方式,那么ExceptionHandlerMethod的信息怎么初始化的呢?
当前Controller
对每个请求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
}protected void detectAnnotationExceptionMappings(Method method, List
}
@ControllerAdvice类
对@ControllerAdvice统一切面类的处理,则是在ExceptionHandlerExceptionResolver的初始化方法afterPropertiesSet中进行处理。
public void afterPropertiesSet() {// Do this first, it may add ResponseBodyAdvice beans// 初始化@ControllerAdvice中的@ExceptionHandlerinitExceptionHandlerAdviceCache();if (this.argumentResolvers == null) {List
}
initExceptionHandlerAdviceCache方法遍历上下文中所有有@ControllerAdvice注解的Bean,然后实例化成ExceptionHandlerMethodResolver类,在构造函数中初始化所有@ExceptionHandler。
private void initExceptionHandlerAdviceCache() {if (getApplicationContext() == null) {return;}if (logger.isDebugEnabled()) {logger.debug("Looking for exception mappings: " + getApplicationContext());}// 查询所有@ControllerAdvice的BeanList
}
匹配到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来对状态码异常和常见的请求响应异常进行统一处理。
4.web.xml的配置某些情况下,SpringMVC的处理并没有异常出现,但在最终的视图输出时找不到视图文件,就会显示404错误页面,非常影响用户体验。我们可以在web.xml中对未捕获的异常以及此种4xx或5xx的异常通过
通常对于系统中的异常,业务相关的尽量自定义异常处理方式,而一些系统异常通过统一错误页面进行处理。