美文网首页征服SpringJava 杂谈阿里云
Spring MVC ControllerAdvice深入解析

Spring MVC ControllerAdvice深入解析

作者: 良辰美景TT | 来源:发表于2018-08-10 11:51 被阅读10次

      Spring 在3.2版本后面增加了一个ControllerAdvice注解。网上的资料说的都是ControllerAdvice配合ExceptionHandler注解可以统一处理异常。而Spring MVC是如何做到的资料却比较少,下面会先给出使用的例子和踩过的一个坑。然后进行相应的源码分析,之后再介始ControllerAdvice另外的两种使用方式。

    ControllerAdvice的简单使用

    • ControllerAdvice配合ExceptionHandler可以统一处理系统的异常,我们先定义一个ExceptionAdvice类用于处理系统的两种类型的异常。代码如下:
    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, "数组越界异常");
    
            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>
    
        <!-- 自定义参数转换 -->
        <bean id="conversionService"
            class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        </bean>
    
    </beans>
    

    遇到的一个坑是当spring mvc配置文件不用<mvc:annotation-drive>这个标签而是手动将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就不会对异常进行拦截呢而通过<mvc:annotation-drive>这个标签就可以呢?我们从Spring MVC的入口看一下异常是如何处理的。下面是关键代码(关键代码都有相应的注释):

    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.
    //根据请求的request得到HandlerExecutionChain 对象,里面有Inceptor和相应的Controller
                    mappedHandler = getHandler(processedRequest);
                    if (mappedHandler == null || mappedHandler.getHandler() == null) {
                        noHandlerFound(processedRequest, response);
                        return;
                    }
    
                    // Determine handler adapter for the current request.
    //根据配置的HandlerAdapter 对handler进行适配
                    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. 
    //这里会调用具体的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;
    //当controller抛出异常后,就会执行下面的逻辑啦
            if (exception != null) {
                if (exception instanceof ModelAndViewDefiningException) {
                    logger.debug("ModelAndViewDefiningException encountered", exception);
                    mv = ((ModelAndViewDefiningException) exception).getModelAndView();
                }
                else {
    //各种不同的异常会走到这里来处理,processHandlerException的源码在下面有详细的注释
                    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 来处理具体的异常,而当我们手动只配置RequestMappingHandlerMapping和RequestMappingHandlerAdapter时,并没有配置任何的HandlerExceptionResolver 。也就是为什么ControllerAdvice不会对异常进行处理了,我们同时也可以想到<mvc:annotation-drive>一定是帮助我们注入了一个HandlerExceptionResolver 类。下面我们通过分析AnnotationDrivenBeanDefinitionParser这个类来看看到底给我们注入的是那个HandlerExceptionResolver,AnnotationDrivenBeanDefinitionParser类就是用于解析<mvc:annotation-drive>标签的。下面是AnnotationDrivenBeanDefinitionParser的部分源码:

    package org.springframework.web.servlet.config;
    
    class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
    
    /**
    *parse是这个类的核心方法,它用于解析 annotation-drive标签里的内容,根据标签里的内容往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转成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里的关键代码:

    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 容器调用的
        @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容器里找出标注了@ControllerAdvice的类,如果有方法标注了@ExceptionHandler会生成一个ExceptionHandlerMethodResolver类用于处理异常并放到exceptionHandlerAdviceCache这个map缓存类里。
    **/
        private void initExceptionHandlerAdviceCache() {
            if (getApplicationContext() == null) {
                return;
            }
            if (logger.isDebugEnabled()) {
                logger.debug("Looking for exception mappings: " + getApplicationContext());
            }
    //这里会找到容器里标注了ControllerAdvice标签的类
            List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
            AnnotationAwareOrderComparator.sort(adviceBeans);
    
            for (ControllerAdviceBean adviceBean : adviceBeans) {
    //这个构造方法里会检查ControllerAdvice类里是否有@ExceptionHandler标注的方法,在ExceptionHandlerMethodResolver 有个异常的map。
                ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType());
                if (resolver.hasExceptionMappings()) {
    //如果有@ExceptionHandler方法,会执行下面的逻辑
                    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);
                    }
                }
            }
        }
    
    /**
    ** 这个方法会根据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();
    //根据具体的异常找到处理异常的方法,然后调用
                    Method method = resolver.resolveMethod(exception);
                    if (method != null) {
                        return new ServletInvocableHandlerMethod(entry.getKey().resolveBean(), method);
                    }
                }
            }
    
            return null;
        }
    }
    

      ExceptionHandlerExceptionResolver这个类首先会扫描容器里所有的ControllerAdvice,如果ControllerAdvice标注了@ExceptionHandler会加到一个map缓存里。在处理具体的异常的时候,会去这个缓存里一个个找是否有ControllerAdvice能够处理这个异常。整个流程我们就分析到这里,下面看看ControllerAdvice的另外两个用法。

    RequestBodyAdvice与ResponseBodyAdvice

      Spring在4.2的版本给我们提供了RequestBodyAdvice与ResponseBodyAdvice这两个接口,而ControllerAdvice是在3.2这个版本里的。那RequestBodyAdvice和ResponseBodyAdvice能够帮我们做些什么事性呢?假如现在有个需求,正常接口返回的是json,但传入的请求头里有callback参数需要返回jsonp格式的数据需要如何做呢?下面我们来看看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方法用于决定是否调用下面的方法,
        boolean supports(MethodParameter methodParameter, Type targetType,
                Class<? extends HttpMessageConverter<?>> converterType);
    
    //处理空参数据情况
        Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
                Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
    
    //在参数读取之前处理的逻辑
        HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
                Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException;
    
    //在参数读取之后处理的逻辑
        Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
                Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
    
    }
    
    

      从RequestBodyAdvice的定义我们可以清楚的看出他主要用于处理Spring MVC请求参数相关的逻辑,首先定义了support方法用于判断是否能够对请求参数做进一步的处理,然后定义了在读取参数前后方法分别用于处理请求参数。这里的读取前后是在Spring MVC调用了HttpMessageConverter对参数进行了转义,所以使用起来还是很方便的。下面来看看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> {
    
    //这个方法用于判断是否需要调用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用于对写入的数据进行修改,通过ResponseBodyAdvice我们可以很方便的将json数据改成jsonp进行返回。下面我自定义了一个JsonpAdvice用于处理根据header参数返回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.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();
            // 根据 header是否有callback参数决定是否返回jsonp格式的数据
            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作为类型转换器。上面的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");
        }
    
    }
    

    总结:

    • Spring MVC通过@ControllerAdvice配合@ExceptionHandler能够统一处理系统的异常信息。
    • ControllerAdvice配合RequestBodyAdvice与ResponseBodyAdvice可以方便的对请求参数与返回值进行修改。

    相关文章

      网友评论

        本文标题:Spring MVC ControllerAdvice深入解析

        本文链接:https://www.haomeiwen.com/subject/bhacbftx.html