SpringMVC源码(五)

SpringMVC除了对请求URL的路由处理特别方便外,还支持对异常的统一处理机制,可以对业务操作时抛出的异常,unchecked异常以及状态码的异常进行统一处理。SpringMVC既提供简单的配置类,也提供了细粒度的异常控制机制。

SpringMVC中所有的异常处理通过接口HandlerExceptionResolver来实现,接口中只定义了一个方法

1public interface HandlerExceptionResolver { 2 3 ModelAndView resolveException( 4 HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex); 5}

方法中接受request和response信息,以及当前的处理Handler,和抛出的异常对象。并且提供抽象类AbstractHandlerExceptionResolver,实现resolveException方法,支持前置判断和处理,将实际处理抽象出doResolveException方法由子类来实现。

1.SimpleMappingExceptionResolver

SimpleMappingExceptionResolver是SpringMVC提供的一个非常便捷的简易异常处理方式,在XML中进行配置即可使用。

1<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> 2 <!-- 默认异常视图 --> 3 <property name="defaultErrorView" value="error"/> 4 <!-- 视图中获取exception信息变量名 --> 5 <property name="exceptionAttribute" value="ex"></property> 6 <!-- 异常同视图映射关系 --> 7 <property name="exceptionMappings"> 8 <props> 9 <prop key="com.lcifn.springmvc.exception.BusinessException">businessEx</prop> 10 </props> 11 </property> 12</bean>

这是极简的一种配置,exceptionMappings配置的是异常同视图之间的映射关系,它是一个Properties对象,key-value分别是异常的类路径和视图名称。defaultErrorView表示默认异常视图,如果抛出的异常没有匹配到任何视图,即会走默认异常视图。exceptionAttribute表示在视图中获取exception信息变量名,默认为exception。还有一些其他配置可以查看SimpleMappingExceptionResolver的源码来使用。

2.@ExceptionHandler

SpringMVC提供了一种注解方式来灵活地配置异常处理,@ExceptionHandler中可以配置要处理的异常类型,然后定义在处理此种异常的方法上,方法只要写在Controller中,即可对Controller中所有请求方法有效。

我们定义一个BaseController,并且将需要处理的异常通过@ExceptionHandler定义好处理方法,这样业务Controller只需要继承这个基类就可以了。处理方法中支持Request/Response/Sessioin等相关的参数绑定。

1[@Controller](https://my.oschina.net/u/1774615) 2public class BaseController { 3 4 @ExceptionHandler(RuntimeException.class) 5 public ModelAndView handleRuntimeException(HttpServletRequest req, HttpServletResponse resp, RuntimeException ex){ 6 return new ModelAndView("error"); 7 } 8}

但是继承的方式还是对业务代码造成侵入,Spring非常重要的特性就是非侵入性,因而SpringMVC提供了@ControllerAdvice,简单来说就是Controller的切面,支持对可选择的Controller进行统一配置,用于异常处理简直再合适不过了,我们只需要将BaseController稍稍改一下。

1@ControllerAdvice 2public class AdviceController { 3 4 @ExceptionHandler(RuntimeException.class) 5 public ModelAndView handleRuntimeException(HttpServletRequest req, HttpServletResponse resp, RuntimeException ex){ 6 return new ModelAndView("error"); 7 } 8}

只需要在统一配置类上加上@ControllerAdvice注解,支持包路径,注解等过滤方式,即可完成对所有业务Controller进行控制,而业务Controller不用做anything。

如果请求为ajax方式,需要其他格式返回异常,在方法上加上@ResponseBody即可。

3.异常处理原理

上面介绍了常用的两种异常处理的配置方式,所谓知其然要知其所以然,SpringMVC怎么在请求处理的过程中完成对异常的统一处理的呢?我们从源码来深度解读。

回到DispatcherServlet的doDispatcher方法

1try { 2 processedRequest = checkMultipart(request); 3 multipartRequestParsed = (processedRequest != request); 4 5 // Determine handler for the current request. 6 mappedHandler = getHandler(processedRequest); 7 if (mappedHandler == null || mappedHandler.getHandler() == null) { 8 noHandlerFound(processedRequest, response); 9 return; 10 } 11 12 // Determine handler adapter for the current request. 13 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); 14 15 if (!mappedHandler.applyPreHandle(processedRequest, response)) { 16 return; 17 } 18 19 // Actually invoke the handler. 20 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); 21 22 if (asyncManager.isConcurrentHandlingStarted()) { 23 return; 24 } 25 26 applyDefaultViewName(processedRequest, mv); 27 mappedHandler.applyPostHandle(processedRequest, response, mv); 28} 29catch (Exception ex) { 30 dispatchException = ex; 31} 32catch (Throwable err) { 33 dispatchException = new NestedServletException("Handler dispatch failed", err); 34} 35processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

可以看到对请求处理的核心处理使用一个大的try/catch,如果出现异常,统一封装成dispatchException交给processDispatchResult方法进行处理。我们知道processDispatchResult方法用来对返回视图进行操作,而同时也对异常进行统一处理。

在processDispatchResult中,首先对异常进行判断。

1if (exception != null) { 2 if (exception instanceof ModelAndViewDefiningException) { 3 logger.debug("ModelAndViewDefiningException encountered", exception); 4 mv = ((ModelAndViewDefiningException) exception).getModelAndView(); 5 } 6 else { 7 Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); 8 mv = processHandlerException(request, response, handler, exception); 9 errorView = (mv != null); 10 } 11}

如果不是特殊的ModelAndViewDefiningException,则由processHandlerException来操作。

1protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, 2 Object handler, Exception ex) throws Exception { 3 4 // Check registered HandlerExceptionResolvers... 5 ModelAndView exMv = null; 6 // 遍历所有注册的异常处理器,由异常处理器进行处理 7 for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) { 8 exMv = handlerExceptionResolver.resolveException(request, response, handler, ex); 9 if (exMv != null) { 10 break; 11 } 12 } 13 // 如果异常视图存在,则转向异常视图 14 if (exMv != null) { 15 if (exMv.isEmpty()) { 16 request.setAttribute(EXCEPTION_ATTRIBUTE, ex); 17 return null; 18 } 19 // We might still need view name translation for a plain error model... 20 if (!exMv.hasView()) { 21 exMv.setViewName(getDefaultViewName(request)); 22 } 23 if (logger.isDebugEnabled()) { 24 logger.debug("Handler execution resulted in exception - forwarding to resolved error view: " + exMv, ex); 25 } 26 WebUtils.exposeErrorRequestAttributes(request, ex, getServletName()); 27 return exMv; 28 } 29 30 throw ex; 31}

我们主要关注异常处理器对异常的处理,SpringMVC通过HandlerExceptionResolver的resolveException调用实现类的实际实现方法doResolveException。

SimpleMappingExceptionResolver

来看SimpleMappingExceptionResolver的实现:

1protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, 2 Object handler, Exception ex) { 3 4 // Expose ModelAndView for chosen error view. 5 // 根据request和异常对象获取异常视图名称 6 String viewName = determineViewName(ex, request); 7 if (viewName != null) { 8 // Apply HTTP status code for error views, if specified. 9 // Only apply it if we're processing a top-level request. 10 Integer statusCode = determineStatusCode(request, viewName); 11 if (statusCode != null) { 12 applyStatusCodeIfPossible(request, response, statusCode); 13 } 14 // 组装异常视图模型ModelAndView 15 return getModelAndView(viewName, ex, request); 16 } 17 else { 18 return null; 19 } 20}

determineViewName方法决定异常视图名称,getModelAndView方法返回ModelAndView对象

1protected String determineViewName(Exception ex, HttpServletRequest request) { 2 String viewName = null; 3 if (this.excludedExceptions != null) { 4 for (Class<?> excludedEx : this.excludedExceptions) { 5 if (excludedEx.equals(ex.getClass())) { 6 return null; 7 } 8 } 9 } 10 // Check for specific exception mappings. 11 if (this.exceptionMappings != null) { 12 viewName = findMatchingViewName(this.exceptionMappings, ex); 13 } 14 // Return default error view else, if defined. 15 if (viewName == null && this.defaultErrorView != null) { 16 if (logger.isDebugEnabled()) { 17 logger.debug("Resolving to default view '" + this.defaultErrorView + "' for exception of type [" + 18 ex.getClass().getName() + "]"); 19 } 20 viewName = this.defaultErrorView; 21 } 22 return viewName; 23}

在determineViewName方法中,我们配置的defaultErrorView和exceptionMappings都起了作用。更细节的就不深入了,有兴趣可以自己去看。

ExceptionHandlerExceptionResolver

ExceptionHandlerExceptionResolver支持了@ExceptionHandler注解的实现。它的抽象基类AbstractHandlerMethodExceptionResolver继承了AbstractHandlerExceptionResolver,doResolveException方法实际调用ExceptionHandlerExceptionResolver的doResolveHandlerMethodException方法。

1protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, 2 HttpServletResponse response, HandlerMethod handlerMethod, Exception exception) { 3 4 // 根据HandlerMethod和exception获取异常处理的Method 5 ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception); 6 if (exceptionHandlerMethod == null) { 7 return null; 8 } 9 10 // 设置异常处理方法的参数解析器和返回值解析器 11 exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); 12 exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); 13 14 ServletWebRequest webRequest = new ServletWebRequest(request, response); 15 ModelAndViewContainer mavContainer = new ModelAndViewContainer(); 16 17 // 执行异常处理方法 18 try { 19 if (logger.isDebugEnabled()) { 20 logger.debug("Invoking @ExceptionHandler method: " + exceptionHandlerMethod); 21 } 22 Throwable cause = exception.getCause(); 23 if (cause != null) { 24 // Expose cause as provided argument as well 25 exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod); 26 } 27 else { 28 // Otherwise, just the given exception as-is 29 exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod); 30 } 31 } 32 catch (Throwable invocationEx) { 33 // Any other than the original exception is unintended here, 34 // probably an accident (e.g. failed assertion or the like). 35 if (invocationEx != exception && logger.isWarnEnabled()) { 36 logger.warn("Failed to invoke @ExceptionHandler method: " + exceptionHandlerMethod, invocationEx); 37 } 38 // Continue with default processing of the original exception... 39 return null; 40 } 41 42 // 对返回的视图模型进行处理 43 if (mavContainer.isRequestHandled()) { 44 return new ModelAndView(); 45 } 46 else { 47 ModelMap model = mavContainer.getModel(); 48 HttpStatus status = mavContainer.getStatus(); 49 ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status); 50 mav.setViewName(mavContainer.getViewName()); 51 if (!mavContainer.isViewReference()) { 52 mav.setView((View) mavContainer.getView()); 53 } 54 if (model instanceof RedirectAttributes) { 55 Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes(); 56 request = webRequest.getNativeRequest(HttpServletRequest.class); 57 RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes); 58 } 59 return mav; 60 } 61}

我们主要关注的是如何匹配到异常处理方法的

1protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) { 2 Class<?> handlerType = (handlerMethod != null ? handlerMethod.getBeanType() : null); 3 4 // 从当前Controller中匹配异常处理Method 5 if (handlerMethod != null) { 6 ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType); 7 if (resolver == null) { 8 resolver = new ExceptionHandlerMethodResolver(handlerType); 9 this.exceptionHandlerCache.put(handlerType, resolver); 10 } 11 Method method = resolver.resolveMethod(exception); 12 if (method != null) { 13 return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method); 14 } 15 } 16 17 // 从ControllerAdvice中匹配异常处理Method 18 for (Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) { 19 if (entry.getKey().isApplicableToBeanType(handlerType)) { 20 ExceptionHandlerMethodResolver resolver = entry.getValue(); 21 Method method = resolver.resolveMethod(exception); 22 if (method != null) { 23 return new ServletInvocableHandlerMethod(entry.getKey().resolveBean(), method); 24 } 25 } 26 } 27 28 return null; 29}

匹配异常处理方法的来源有两个,一个是当前Controller,一个是所有@ControllerAdvice类。可以看到两种方式都使用了cache的方式,那么ExceptionHandlerMethod的信息怎么初始化的呢?

当前Controller

对每个请求HandlerMethod的Controller类型,都实例化一个ExceptionHandlerMethodResolver来处理异常。ExceptionHandlerMethodResolver的构造函数中初始化了当前Controller中的异常处理配置。

1public ExceptionHandlerMethodResolver(Class<?> handlerType) { 2 for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) { 3 // detectExceptionMappings方法执行探查 4 for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) { 5 addExceptionMapping(exceptionType, method); 6 } 7 } 8} 9 10private List<Class<? extends Throwable>> detectExceptionMappings(Method method) { 11 List<Class<? extends Throwable>> result = new ArrayList<Class<? extends Throwable>>(); 12 // 探查所有ExceptionHandler注解的方法 13 detectAnnotationExceptionMappings(method, result); 14 if (result.isEmpty()) { 15 for (Class<?> paramType : method.getParameterTypes()) { 16 if (Throwable.class.isAssignableFrom(paramType)) { 17 result.add((Class<? extends Throwable>) paramType); 18 } 19 } 20 } 21 if (result.isEmpty()) { 22 throw new IllegalStateException("No exception types mapped to " + method); 23 } 24 return result; 25} 26 27protected void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) { 28 ExceptionHandler ann = AnnotationUtils.findAnnotation(method, ExceptionHandler.class); 29 result.addAll(Arrays.asList(ann.value())); 30}

@ControllerAdvice类

对@ControllerAdvice统一切面类的处理,则是在ExceptionHandlerExceptionResolver的初始化方法afterPropertiesSet中进行处理。

1public void afterPropertiesSet() { 2 // Do this first, it may add ResponseBodyAdvice beans 3 // 初始化@ControllerAdvice中的@ExceptionHandler 4 initExceptionHandlerAdviceCache(); 5 6 if (this.argumentResolvers == null) { 7 List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers(); 8 this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); 9 } 10 if (this.returnValueHandlers == null) { 11 List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers(); 12 this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); 13 } 14}

initExceptionHandlerAdviceCache方法遍历上下文中所有有@ControllerAdvice注解的Bean,然后实例化成ExceptionHandlerMethodResolver类,在构造函数中初始化所有@ExceptionHandler。

1private void initExceptionHandlerAdviceCache() { 2 if (getApplicationContext() == null) { 3 return; 4 } 5 if (logger.isDebugEnabled()) { 6 logger.debug("Looking for exception mappings: " + getApplicationContext()); 7 } 8 9 // 查询所有@ControllerAdvice的Bean 10 List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); 11 AnnotationAwareOrderComparator.sort(adviceBeans); 12 13 // 遍历,实例化ExceptionHandlerMethodResolver 14 for (ControllerAdviceBean adviceBean : adviceBeans) { 15 ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType()); 16 if (resolver.hasExceptionMappings()) { 17 this.exceptionHandlerAdviceCache.put(adviceBean, resolver); 18 if (logger.isInfoEnabled()) { 19 logger.info("Detected @ExceptionHandler methods in " + adviceBean); 20 } 21 } 22 23 if (ResponseBodyAdvice.class.isAssignableFrom(adviceBean.getBeanType())) { 24 this.responseBodyAdvice.add(adviceBean); 25 if (logger.isInfoEnabled()) { 26 logger.info("Detected ResponseBodyAdvice implementation in " + adviceBean); 27 } 28 } 29 } 30}

匹配到exceptionHandlerMethod后,设置一些方法执行的环境,然后调用ServletInvocableHandlerMethod中的invokeAndHandle去执行,这个调用过程和正常请求的调用就是一致了。这里也不向下扩展了,可以参看SpringMVC源码(四)-请求处理

1public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, 2 Object... providedArgs) throws Exception { 3 4 // 执行请求方法 5 Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); 6 setResponseStatus(webRequest); 7 8 if (returnValue == null) { 9 if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) { 10 mavContainer.setRequestHandled(true); 11 return; 12 } 13 } 14 else if (StringUtils.hasText(getResponseStatusReason())) { 15 mavContainer.setRequestHandled(true); 16 return; 17 } 18 19 mavContainer.setRequestHandled(false); 20 try { 21 this.returnValueHandlers.handleReturnValue( 22 returnValue, getReturnValueType(returnValue), mavContainer, webRequest); 23 } 24 catch (Exception ex) { 25 if (logger.isTraceEnabled()) { 26 logger.trace(getReturnValueHandlingErrorMessage("Error handling return value", returnValue), ex); 27 } 28 throw ex; 29 } 30}

至此ExceptionHandlerExceptionResolver的异常处理已经基本完成。SpringMVC还内置了ResponseStatusExceptionResolver和DefaultHandlerExceptionResolver来对状态码异常和常见的请求响应异常进行统一处理。

4.web.xml的配置

某些情况下,SpringMVC的处理并没有异常出现,但在最终的视图输出时找不到视图文件,就会显示404错误页面,非常影响用户体验。我们可以在web.xml中对未捕获的异常以及此种4xx或5xx的异常通过<error-page>进行处理。

1<error-page> 2 <error-code>404</error-code> 3 <location>/404</location> 4</error-page> 5<error-page> 6 <exception-type>java.lang.Throwable</exception-type> 7 <location>/500</location> 8</error-page>

通常对于系统中的异常,业务相关的尽量自定义异常处理方式,而一些系统异常通过统一错误页面进行处理。

点赞
收藏

评论区

加载中...

相关推荐

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

03.Android崩溃Crash库之ExceptionHandler分析

目录总结00.异常处理几个常用api01.UncaughtExceptionHandler02.Java线程处理异常分析03.Android中线程处理异常分析04.为何使用setDefaultUncaughtExceptionHandler前沿上一篇整体介绍了crash崩溃

Spring异常处理

@ExceptionHandler:统一处理某一类异常,从而能够减少代码重复率和复杂度@ControllerAdvice:异常集中处理,更好的使业务逻辑与异常处理剥离开@ResponseStatus:可以将某种异常映射为HTTP状态码@ControllerAdvicepublicclassExceptio

Spring Boot @ControllerAdvice+@ExceptionHandler处理controller异常

需求:  1.springboot 项目restful 风格统一放回json  2.不在controller写trycatch代码块简洁controller层  3.对异常做统一处理,同时处理@Validated校验器注解的异常方法:  @ControllerAdvice注解定义全局异常处理类@ControllerAdvice

SpringMVC 异常处理

基本概念在SpringMVC中HandlerExceptionResolver接口负责统一异常处理。内部构造下面来看它的源码:publicinterfaceHandlerExceptionResolver{ModelAndViewresolveException(H

初探 Objective

作者:Cyandev,iOS和MacOS开发者,目前就职于字节跳动0x00前言异常处理是许多高级语言都具有的特性,它可以直接中断当前函数并将控制权转交给能够处理异常的函数。不同语言在异常处理的实现上各不相同,本文主要来分析一下ObjectiveC和C这两个语言。为什么要把ObjectiveC和