SpringMVC源码(五)

Easter79
• 阅读 707

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.SimpleMappingExceptionResolver

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

<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的源码来使用。

2.@ExceptionHandler

SpringMVC提供了一种注解方式来灵活地配置异常处理,@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);
        }
        // 组装异常视图模型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

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的信息怎么初始化的呢?

当前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<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类

对@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来对状态码异常和常见的请求响应异常进行统一处理。

4.web.xml的配置

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

<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>

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

点赞
收藏
评论区
推荐文章
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
3年前
03.Android崩溃Crash库之ExceptionHandler分析
目录总结00.异常处理几个常用api01.UncaughtExceptionHandler02.Java线程处理异常分析03.Android中线程处理异常分析04.为何使用setDefaultUncaughtExceptionHandler前沿上一篇整体介绍了crash崩溃
Easter79 Easter79
3年前
Spring异常处理
@ExceptionHandler:统一处理某一类异常,从而能够减少代码重复率和复杂度@ControllerAdvice:异常集中处理,更好的使业务逻辑与异常处理剥离开@ResponseStatus:可以将某种异常映射为HTTP状态码@ControllerAdvicepublicclassExceptio
Stella981 Stella981
3年前
Spring Boot @ControllerAdvice+@ExceptionHandler处理controller异常
需求:  1.springboot 项目restful 风格统一放回json  2.不在controller写trycatch代码块简洁controller层  3.对异常做统一处理,同时处理@Validated校验器注解的异常方法:  @ControllerAdvice注解定义全局异常处理类@ControllerAdvice
Easter79 Easter79
3年前
SpringMVC 异常处理
基本概念在SpringMVC中HandlerExceptionResolver接口负责统一异常处理。内部构造下面来看它的源码:publicinterfaceHandlerExceptionResolver{ModelAndViewresolveException(H
Wesley13 Wesley13
3年前
初探 Objective
作者:Cyandev,iOS和MacOS开发者,目前就职于字节跳动0x00前言异常处理是许多高级语言都具有的特性,它可以直接中断当前函数并将控制权转交给能够处理异常的函数。不同语言在异常处理的实现上各不相同,本文主要来分析一下ObjectiveC和C这两个语言。为什么要把ObjectiveC和
Stella981 Stella981
3年前
Hystrix异常处理及线程池划分
异常处理异常传播在HystrixCommand实现的run()方法中抛出异常时,除了HystrixBadRequestException之外,其他异常均会被Hystrix认为命令执行失败并触发服务降级的处理逻辑,所以当需要在命令执行中抛出不触发服务降级的异常时来选择它。在使用注解配置实现Hystrix命令时,可以忽略指定的异常
Easter79 Easter79
3年前
SpringBoot2.0 基础案例(03):配置系统全局异常映射处理
本文源码GitHub:知了一笑https://github.com/cicadasmile/springbootbase一、异常分类这里的异常分类从系统处理异常的角度看,主要分类两类:业务异常和系统异常。1、业务异常业务异常主要是一些可预见性异常,处理业务异常,用来提示用户
Stella981 Stella981
3年前
SpringBoot2.0 基础案例(03):配置系统全局异常映射处理
本文源码GitHub:知了一笑https://github.com/cicadasmile/springbootbase一、异常分类这里的异常分类从系统处理异常的角度看,主要分类两类:业务异常和系统异常。1、业务异常业务异常主要是一些可预见性异常,处理业务异常,用来提示用户
Easter79 Easter79
3年前
SpringMVC 异常处理.
一、异常处理  Spring提供了多种方式将异常转换为响应:特定的Spring异常将会自动映射为指定的HTTP状态码。在默认情况下,Spring会将自身的一些异常自动转换为合适的状态码,从而反馈给客户端。实际上,如果没有出现任何映射的异常,响应都会带有500状态码。映射表如下:!(https://oscimg.os
Easter79
Easter79
Lv1
今生可爱与温柔,每一样都不能少。
文章
2.8k
粉丝
5
获赞
1.2k