Spring MVC ControllerAdvice深入解析

??Spring 在3.2版本后面增加了一個ControllerAdvice注解。網(wǎng)上的資料說的都是ControllerAdvice配合ExceptionHandler注解可以統(tǒng)一處理異常牡拇。而Spring MVC是如何做到的資料卻比較少匙奴,下面會先給出使用的例子和踩過的一個坑袖订。然后進(jìn)行相應(yīng)的源碼分析,之后再介始ControllerAdvice另外的兩種使用方式。

ControllerAdvice的簡單使用

  • ControllerAdvice配合ExceptionHandler可以統(tǒng)一處理系統(tǒng)的異常,我們先定義一個ExceptionAdvice類用于處理系統(tǒng)的兩種類型的異常桦他。代碼如下:
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import com.pptv.frame.dto.common.ResponseDTO;
import com.pptv.frame.dto.common.ServiceCodeEnum;

@ControllerAdvice
public class ExceptionAdvice {

    @ExceptionHandler({
                        ArrayIndexOutOfBoundsException.class
    })
    @ResponseBody
    public ResponseDTO handleArrayIndexOutOfBoundsException(ArrayIndexOutOfBoundsException e) {
        // TODO 記錄log日志
        e.printStackTrace();
        ResponseDTO responseDTO = new ResponseDTO();
        responseDTO.wrapResponse(ServiceCodeEnum.E999998, "數(shù)組越界異常");

        return responseDTO;
    }

    @ExceptionHandler({
                        Exception.class
    })
    @ResponseBody
    public ResponseDTO handleException(Exception e) {
        // TODO 記錄log日志
        e.printStackTrace();
        ResponseDTO responseDTO = new ResponseDTO();
        responseDTO.wrapResponse(ServiceCodeEnum.E999998, "未知異常");
        return responseDTO;
    }

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

    <!-- 配置Fastjson支持 -->
    <mvc:annotation-driven conversion-service="conversionService">
        <mvc:message-converters register-defaults="true">
            <bean
                class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
                <property name="supportedMediaTypes" value="text/html;charset=UTF-8" />
                <property name="features">
                    <array>
                        <value>WriteMapNullValue</value>
                        <value>WriteNullStringAsEmpty</value>
                    </array>
                </property>
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>

    <!-- 自定義參數(shù)轉(zhuǎn)換 -->
    <bean id="conversionService"
        class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    </bean>

</beans>

遇到的一個坑是當(dāng)spring mvc配置文件不用<mvc:annotation-drive>這個標(biāo)簽而是手動將RequestMappingHandlerMapping與RequestMappingHandlerAdapter這兩個類讓spring容器管理,上面的ControllerAdvice將不起作用

    <bean
        class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" />
    <bean
        class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
        <property name="webBindingInitializer">
            <bean
                class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer">
                <property name="conversionService" ref="conversionService" />
            </bean>
        </property>
        <property name="messageConverters">
            <list>
                <ref bean="mappingJackson2HttpMessageConverter" />
                <ref bean="stringHttpMessageConverter" />
            </list>
        </property>
    </bean>

Spring MVC是如何處理異常的

??下面來看看Spring MVC是如何處理異常的谆棱,為什么我手動配置了RequestMappingHandlerMapping和RequestMappingHandlerAdapter ControllerAdvice就不會對異常進(jìn)行攔截呢而通過<mvc:annotation-drive>這個標(biāo)簽就可以呢快压?我們從Spring MVC的入口看一下異常是如何處理的。下面是關(guān)鍵代碼(關(guān)鍵代碼都有相應(yīng)的注釋):

public class DispatcherServlet extends FrameworkServlet {
/**
  *這個方法是Spring MVC的入口方法垃瞧,可以看到Spring MVC人具體處理流程
 **/
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;

        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

        try {
            ModelAndView mv = null;
            Exception dispatchException = null;
//這里有個try蔫劣,下面的catch就是用于處理異常的
            try {
                              //檢查是否是上傳文件的請求
                processedRequest = checkMultipart(request);
                multipartRequestParsed = (processedRequest != request);

                // Determine handler for the current request.
//根據(jù)請求的request得到HandlerExecutionChain 對象,里面有Inceptor和相應(yīng)的Controller
                mappedHandler = getHandler(processedRequest);
                if (mappedHandler == null || mappedHandler.getHandler() == null) {
                    noHandlerFound(processedRequest, response);
                    return;
                }

                // Determine handler adapter for the current request.
//根據(jù)配置的HandlerAdapter 對handler進(jìn)行適配
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

                // Process last-modified header, if supported by the handler.
                String method = request.getMethod();
                boolean isGet = "GET".equals(method);
                if (isGet || "HEAD".equals(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if (logger.isDebugEnabled()) {
                        logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                    }
                    if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }

                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }

                // Actually invoke the handler. 
//這里會調(diào)用具體的Handler也就是我們寫的Controller
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }

                applyDefaultViewName(processedRequest, mv);
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            }
            catch (Exception ex) {
//上面處理的邏輯有任何的異常个从,都將會落到這里脉幢,用dispatchException 這個變量接住異常引用
                dispatchException = ex;
            }
            catch (Throwable err) {
//如果拋的是error, 這里也會把異常給接住 
                // As of 4.3, we're processing Errors thrown from handler methods as well,
                // making them available for @ExceptionHandler methods and other scenarios.
                dispatchException = new NestedServletException("Handler dispatch failed", err);
            }
//具體處理異常的邏輯看來是在這個方法里了嗦锐,具體的邏輯看下面的源碼
            processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        }
        catch (Exception ex) {
//這是最外面的try嫌松,這里需要處理Inteceptor里After的邏輯
            triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
        }
        catch (Throwable err) {
//這是最外面的try,這里需要處理Inteceptor里After的邏輯
            triggerAfterCompletion(processedRequest, response, mappedHandler,
                    new NestedServletException("Handler processing failed", err));
        }
        finally {
            if (asyncManager.isConcurrentHandlingStarted()) {
                // Instead of postHandle and afterCompletion
                if (mappedHandler != null) {
                    mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                }
            }
            else {
                // Clean up any resources used by a multipart request.
                if (multipartRequestParsed) {
                    cleanupMultipart(processedRequest);
                }
            }
        }
    }

/**
*這個方法里processHandlerException用于處理各種不同的Exception
**/
    private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
            HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {

        boolean errorView = false;
//當(dāng)controller拋出異常后奕污,就會執(zhí)行下面的邏輯啦
        if (exception != null) {
            if (exception instanceof ModelAndViewDefiningException) {
                logger.debug("ModelAndViewDefiningException encountered", exception);
                mv = ((ModelAndViewDefiningException) exception).getModelAndView();
            }
            else {
//各種不同的異常會走到這里來處理萎羔,processHandlerException的源碼在下面有詳細(xì)的注釋
                Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
                mv = processHandlerException(request, response, handler, exception);
                errorView = (mv != null);
            }
        }

        // Did the handler return a view to render?
        if (mv != null && !mv.wasCleared()) {
            render(mv, request, response);
            if (errorView) {
                WebUtils.clearErrorRequestAttributes(request);
            }
        }
        else {
            if (logger.isDebugEnabled()) {
                logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
                        "': assuming HandlerAdapter completed request handling");
            }
        }

        if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
            // Concurrent handling started during a forward
            return;
        }

        if (mappedHandler != null) {
            mappedHandler.triggerAfterCompletion(request, response, null);
        }
    }

/**
**這里是用于處理Spring MVC異常的入口
**/
    protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
            Object handler, Exception ex) throws Exception {

        // Check registered HandlerExceptionResolvers...
//通過注入的handlerExceptionResolvers來處得具體的Exception,這也就找到了我上面踩坑的原因了碳默。
        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;
    }

}

??通過上面的源碼贾陷,我們一步步可以跟蹤到processHandlerException這個方法,這個方法里通過HandlerExceptionResolver 來處理具體的異常嘱根,而當(dāng)我們手動只配置RequestMappingHandlerMapping和RequestMappingHandlerAdapter時昵宇,并沒有配置任何的HandlerExceptionResolver 。也就是為什么ControllerAdvice不會對異常進(jìn)行處理了儿子,我們同時也可以想到<mvc:annotation-drive>一定是幫助我們注入了一個HandlerExceptionResolver 類瓦哎。下面我們通過分析AnnotationDrivenBeanDefinitionParser這個類來看看到底給我們注入的是那個HandlerExceptionResolver,AnnotationDrivenBeanDefinitionParser類就是用于解析<mvc:annotation-drive>標(biāo)簽的柔逼。下面是AnnotationDrivenBeanDefinitionParser的部分源碼:

package org.springframework.web.servlet.config;

class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {

/**
*parse是這個類的核心方法蒋譬,它用于解析 annotation-drive標(biāo)簽里的內(nèi)容,根據(jù)標(biāo)簽里的內(nèi)容往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轉(zhuǎn)成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;
    }
}

??通過上面代碼的分析维咸, 我們可以找到ExceptionHandlerExceptionResolver這個類來用于處理Spring MVC的各種異常剂买,那ExceptionHandlerExceptionResolver具體又是如何跟ControllerAdvice配合使用來處理各種異常的呢?我們來看看ExceptionHandlerExceptionResolver里的關(guān)鍵代碼:

package org.springframework.web.servlet.mvc.method.annotation;

public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
        implements ApplicationContextAware, InitializingBean {
//這里有個map用于保存ControllerAdviceBean
    private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache =
            new LinkedHashMap<ControllerAdviceBean, ExceptionHandlerMethodResolver>();

//這個方法是由spring 容器調(diào)用的
    @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容器里找出標(biāo)注了@ControllerAdvice的類惠爽,如果有方法標(biāo)注了@ExceptionHandler會生成一個ExceptionHandlerMethodResolver類用于處理異常并放到exceptionHandlerAdviceCache這個map緩存類里。
**/
    private void initExceptionHandlerAdviceCache() {
        if (getApplicationContext() == null) {
            return;
        }
        if (logger.isDebugEnabled()) {
            logger.debug("Looking for exception mappings: " + getApplicationContext());
        }
//這里會找到容器里標(biāo)注了ControllerAdvice標(biāo)簽的類
        List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
        AnnotationAwareOrderComparator.sort(adviceBeans);

        for (ControllerAdviceBean adviceBean : adviceBeans) {
//這個構(gòu)造方法里會檢查ControllerAdvice類里是否有@ExceptionHandler標(biāo)注的方法瞬哼,在ExceptionHandlerMethodResolver 有個異常的map婚肆。
            ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType());
            if (resolver.hasExceptionMappings()) {
//如果有@ExceptionHandler方法,會執(zhí)行下面的邏輯
                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);
                }
            }
        }
    }

/**
** 這個方法會根據(jù)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();
//根據(jù)具體的異常找到處理異常的方法坐慰,然后調(diào)用
                Method method = resolver.resolveMethod(exception);
                if (method != null) {
                    return new ServletInvocableHandlerMethod(entry.getKey().resolveBean(), method);
                }
            }
        }

        return null;
    }
}

??ExceptionHandlerExceptionResolver這個類首先會掃描容器里所有的ControllerAdvice,如果ControllerAdvice標(biāo)注了@ExceptionHandler會加到一個map緩存里较性。在處理具體的異常的時候,會去這個緩存里一個個找是否有ControllerAdvice能夠處理這個異常结胀。整個流程我們就分析到這里赞咙,下面看看ControllerAdvice的另外兩個用法。

RequestBodyAdvice與ResponseBodyAdvice

??Spring在4.2的版本給我們提供了RequestBodyAdvice與ResponseBodyAdvice這兩個接口糟港,而ControllerAdvice是在3.2這個版本里的攀操。那RequestBodyAdvice和ResponseBodyAdvice能夠幫我們做些什么事性呢?假如現(xiàn)在有個需求秸抚,正常接口返回的是json崔赌,但傳入的請求頭里有callback參數(shù)需要返回jsonp格式的數(shù)據(jù)需要如何做呢?下面我們來看看RequestBodyAdvice和ResponseBodyAdvice這兩個類的具體定義耸别,RequestBodyAdvice代碼如下:

package org.springframework.web.servlet.mvc.method.annotation;

import java.io.IOException;
import java.lang.reflect.Type;

import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;

public interface RequestBodyAdvice {
//supports方法用于決定是否調(diào)用下面的方法健芭,
    boolean supports(MethodParameter methodParameter, Type targetType,
            Class<? extends HttpMessageConverter<?>> converterType);

//處理空參數(shù)據(jù)情況
    Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
            Type targetType, Class<? extends HttpMessageConverter<?>> converterType);

//在參數(shù)讀取之前處理的邏輯
    HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
            Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException;

//在參數(shù)讀取之后處理的邏輯
    Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
            Type targetType, Class<? extends HttpMessageConverter<?>> converterType);

}

??從RequestBodyAdvice的定義我們可以清楚的看出他主要用于處理Spring MVC請求參數(shù)相關(guān)的邏輯,首先定義了support方法用于判斷是否能夠?qū)φ埱髤?shù)做進(jìn)一步的處理秀姐,然后定義了在讀取參數(shù)前后方法分別用于處理請求參數(shù)慈迈。這里的讀取前后是在Spring MVC調(diào)用了HttpMessageConverter對參數(shù)進(jìn)行了轉(zhuǎn)義,所以使用起來還是很方便的省有。下面來看看ResponseBodyAdvice的定義:

package org.springframework.web.servlet.mvc.method.annotation;

import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;

public interface ResponseBodyAdvice<T> {

//這個方法用于判斷是否需要調(diào)用beforeBodyWrite方法
    boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

//這里在寫入的時候就可以修改要寫入的值啦
    T beforeBodyWrite(T body, MethodParameter returnType, MediaType selectedContentType,
            Class<? extends HttpMessageConverter<?>> selectedConverterType,
            ServerHttpRequest request, ServerHttpResponse response);

}

??ResponseBodyAdvice用于對寫入的數(shù)據(jù)進(jìn)行修改痒留,通過ResponseBodyAdvice我們可以很方便的將json數(shù)據(jù)改成jsonp進(jìn)行返回。下面我自定義了一個JsonpAdvice用于處理根據(jù)header參數(shù)返回jsonp格式的數(shù)據(jù)蠢沿。代碼如下:

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.StringUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.AbstractMappingJacksonResponseBodyAdvice;

/**
 * 處理需要返回jsonp的Advice 功能描述:
 * 
 * @version 2.0.0
 * @author zhiminchen
 */
@ControllerAdvice
public class JsonpAdvice extends AbstractMappingJacksonResponseBodyAdvice {

    @Override
    protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer,
                                           MediaType contentType,
                                           MethodParameter returnType,
                                           ServerHttpRequest request,
                                           ServerHttpResponse response) {

        HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
        // 根據(jù) header是否有callback參數(shù)決定是否返回jsonp格式的數(shù)據(jù)
        String callback = servletRequest.getHeader("callback");
        if (StringUtils.isNotBlank(callback)) {
            MediaType contentTypeToUse = getContentType(contentType, request, response);
            response.getHeaders().setContentType(contentTypeToUse);
            bodyContainer.setJsonpFunction(callback);
        }
    }

    protected MediaType getContentType(MediaType contentType,
                                       ServerHttpRequest request,
                                       ServerHttpResponse response) {
        return new MediaType("application", "javascript");
    }

}

如果采用的是FastJsonHttpMessageConverter作為類型轉(zhuǎn)換器伸头。上面的JsonpAdvice 不起作用, 我們可以再自定義一個爭對FastJsonHttpMessageConverter的Jsonp攔截器舷蟀,代碼如下:

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.StringUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import com.alibaba.fastjson.JSONPObject;
import com.pptv.frame.dto.common.ResponseDTO;

/**
 * 處理需要返回jsonp的Advice 功能描述:
 * 
 * @version 2.0.0
 * @author zhiminchen
 */
@ControllerAdvice
public class JsonpAdvice implements ResponseBodyAdvice {

    @Override
    public boolean supports(MethodParameter returnType,
                            Class converterType) {
        return returnType.getMethod().getReturnType().equals(ResponseDTO.class);
    }

    @Override
    public Object beforeBodyWrite(Object body,
                                  MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class selectedConverterType,
                                  ServerHttpRequest request,
                                  ServerHttpResponse response) {

        HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
        String callback = servletRequest.getHeader("callback");
        if (StringUtils.isNotBlank(callback)) {
            MediaType contentTypeToUse = getContentType(request, response);
            response.getHeaders().setContentType(contentTypeToUse);
            JSONPObject jsonpObject = new JSONPObject(callback);
            jsonpObject.addParameter(body);
            return jsonpObject;
        } else {
            return body;
        }
    }

    protected MediaType getContentType(ServerHttpRequest request,
                                       ServerHttpResponse response) {
        return new MediaType("application", "javascript");
    }

}

總結(jié):

  • Spring MVC通過@ControllerAdvice配合@ExceptionHandler能夠統(tǒng)一處理系統(tǒng)的異常信息恤磷。
  • ControllerAdvice配合RequestBodyAdvice與ResponseBodyAdvice可以方便的對請求參數(shù)與返回值進(jìn)行修改。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末野宜,一起剝皮案震驚了整個濱河市扫步,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌匈子,老刑警劉巖河胎,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異虎敦,居然都是意外死亡游岳,警方通過查閱死者的電腦和手機(jī)政敢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來胚迫,“玉大人喷户,你說我怎么就攤上這事∩吻” “怎么了?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵通贞,是天一觀的道長朗若。 經(jīng)常有香客問我,道長昌罩,這世上最難降的妖魔是什么哭懈? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮茎用,結(jié)果婚禮上遣总,老公的妹妹穿的比我還像新娘。我一直安慰自己轨功,他們只是感情好旭斥,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著古涧,像睡著了一般垂券。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上羡滑,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天菇爪,我揣著相機(jī)與錄音,去河邊找鬼柒昏。 笑死凳宙,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的职祷。 我是一名探鬼主播氏涩,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼有梆!你這毒婦竟也來了削葱?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤淳梦,失蹤者是張志新(化名)和其女友劉穎析砸,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體爆袍,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡首繁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年作郭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片弦疮。...
    茶點(diǎn)故事閱讀 40,110評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡夹攒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出胁塞,到底是詐尸還是另有隱情咏尝,我是刑警寧澤,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布啸罢,位于F島的核電站编检,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏扰才。R本人自食惡果不足惜允懂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望衩匣。 院中可真熱鬧蕾总,春花似錦、人聲如沸琅捏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽柄延。三九已至置侍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間拦焚,已是汗流浹背蜡坊。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留赎败,地道東北人秕衙。 一個月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像僵刮,于是被迫代替她去往敵國和親据忘。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評論 2 355

推薦閱讀更多精彩內(nèi)容