美文网首页
spring升级后Ajax请求出错(406 Not Accept

spring升级后Ajax请求出错(406 Not Accept

作者: 马木木 | 来源:发表于2019-10-12 14:02 被阅读0次

    1.背景

    由于业务需要,今天公司的JDK升级到1.8,容器要求Spring也需要同时升级到4.0+,解决完依赖的问题之后,代码启动成功,页面展示正常,但是遇到Ajax请求的地方就炸了,错误码406,导致请求失败,内容无法正常返回,Debug发现业务代码处理逻辑执行正常,怀疑在Spring对结果的渲染出错,F12分析请求可以发现返回头的内容内容并不是application/json而是text\html,不符合@ResponseBody注解的目的。

    image

    2.分析

    首先进入DispatcherServlet类的doDispatch核心处理

    protected void doDispatch(HttpServletRequest request, HttpServletResponse 
        response) throws Exception {
        
        .....
        // 处理请求和修饰结果的方法
        /**
         * ha 变量是类 RequestMappingHandlerAdapter 的实例
         * 其继承自AbstractHandlerMethodAdapter,ha.handle方法执行的所在类
         * mappedHandler.getHandler() 根据请求地址查询出对应的类.方法
         /
        mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
        .....
    
    }
    

    AbstractHandlerMethodAdapter.handle方法调用抽象方法handleInternal,我们回到子类RequestMappingHandlerAdapter中查看

    @Override
        protected ModelAndView handleInternal(HttpServletRequest request,
                HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
    
            ModelAndView mav;
            checkRequest(request);
    
            // Execute invokeHandlerMethod in synchronized block if required.
            if (this.synchronizeOnSession) {
                HttpSession session = request.getSession(false);
                if (session != null) {
                    Object mutex = WebUtils.getSessionMutex(session);
                    synchronized (mutex) {
                        mav = invokeHandlerMethod(request, response, handlerMethod);
                    }
                }
                else {
                    // No HttpSession available -> no mutex necessary
                    mav = invokeHandlerMethod(request, response, handlerMethod);
                }
            }
            else {
                // No synchronization on session demanded at all...
                mav = invokeHandlerMethod(request, response, handlerMethod);
            }
    
            if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
                if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
                    applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
                }
                else {
                    prepareResponse(response);
                }
            }
    
            return mav;
        }
    

    可以发现不管怎样都需要走invokeHandlerMethod(request, response, handlerMethod)这个方法,这个也就是我们需要跟踪的方法

    protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
                HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
    
            ServletWebRequest webRequest = new ServletWebRequest(request, response);
            try {
                WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
                ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
                // 这边主要为接下来的处理放入一些参数处理和返回值处理的处理器
                ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
                invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
                invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
                invocableMethod.setDataBinderFactory(binderFactory);
                invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
                ...........
                ...........
                if (asyncManager.hasConcurrentResult()) {
                    Object result = asyncManager.getConcurrentResult();
                    mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
                    asyncManager.clearConcurrentResult();
                    if (logger.isDebugEnabled()) {
                        logger.debug("Found concurrent result value [" + result + "]");
                    }
                    invocableMethod = invocableMethod.wrapConcurrentResult(result);
                }
    
                // 这边是我们的主要的处理方法
                invocableMethod.invokeAndHandle(webRequest, mavContainer);
                if (asyncManager.isConcurrentHandlingStarted()) {
                    return null;
                }
    
                return getModelAndView(mavContainer, modelFactory, webRequest);
            }
            finally {
                webRequest.requestCompleted();
            }
        }
    

    invocableMethod.invokeAndHandle(webRequest, mavContainer);是主要的处理逻辑这里边包含了请求的处理,和返回值的装饰

    public void invokeAndHandle(ServletWebRequest webRequest,
                ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
            // 这里边包含了请求参数转换为方法参数,并且反射调用相应的方法也就是我们的
            // 业务代码来处理请求,并获取返回值,returnValue就是方法的返回值
            // 这次主要是分析对返回值的处理就不做分析了
            Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
            setResponseStatus(webRequest);
    
            if (returnValue == null) {
                if (isRequestNotModified(webRequest) || hasResponseStatus() || mavContainer.isRequestHandled()) {
                    mavContainer.setRequestHandled(true);
                    return;
                }
            }
            else if (StringUtils.hasText(this.responseReason)) {
                mavContainer.setRequestHandled(true);
                return;
            }
    
            mavContainer.setRequestHandled(false);
            try {
            // 这边是对返回值的处理,返回json还是渲染页面都是这边的,看名字也能看出来
            // getReturnValueType(returnValue)方法是分析返回值的包装下
                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;
            }
        }
    
    @Override
        public void handleReturnValue(Object returnValue, MethodParameter returnType,
                ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
    
            HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
            if (handler == null) {
                throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
            }
            handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
        }
    

    从前边注册的返回值处理器中选择正确的处理器并处理请求,debug发现注册的处理器有15中

    image

    由于我们是有注解@ResponseBody,我们的处理器就是RequestResponseBodyMethodProcessor

    public void handleReturnValue(Object returnValue, MethodParameter returnType,
                ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
                throws IOException, HttpMediaTypeNotAcceptableException {
    
            mavContainer.setRequestHandled(true);
            if (returnValue != null) {
                // 这边走
                writeWithMessageConverters(returnValue, returnType, webRequest);
            }
        }
    
    protected <T> void writeWithMessageConverters(T value, MethodParameter returnType,
                ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
                throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
    
            Object outputValue;
            Class<?> valueType;
            Type declaredType;
    
            if (value instanceof CharSequence) {
                outputValue = value.toString();
                valueType = String.class;
                declaredType = String.class;
            }
            else {
                outputValue = value;
                        // 返回值得类型 我这边是ArrayList
                valueType = getReturnValueType(outputValue, returnType);
                declaredType = getGenericType(returnType);
            }
    
            HttpServletRequest request = inputMessage.getServletRequest();
            // 请求要求的内容类型,这边3.0和4.0的有较大的区别,
            //也是导致升级后不可用的原因
            List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
            // 可处理返回值类型的处理器可以接受的返回值类型
            List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);
    
            if (outputValue != null && producibleMediaTypes.isEmpty()) {
                throw new IllegalArgumentException("No converter found for return value of type: " + valueType);
            }
    
            Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
            for (MediaType requestedType : requestedMediaTypes) {
                for (MediaType producibleType : producibleMediaTypes) {
                    if (requestedType.isCompatibleWith(producibleType)) {
                        compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType));
                    }
                }
            }
            // 匹配不到就抛出异常 也是我们的异常的产生源
            if (compatibleMediaTypes.isEmpty()) {
                if (outputValue != null) {
                    throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
                }
                return;
            }
            ...................
            ...................
        }
    
    

    getAcceptableMediaTypes()这个获取请求的的content-type类型3.0和4.0存在较大的区别,3.0是直接通过请求头来获取的,而4.0经历了内容协商器这个处理器,这个处理器就是
    ``

    private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
            List<MediaType> mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
            return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes);
        }
    
    @Override
        public List<MediaType> resolveMediaTypes(NativeWebRequest request)
                throws HttpMediaTypeNotAcceptableException {
    
    /**
      * strategies 注册了两个处理器
      * ServletPathExtensionContentNegotiationStrategy即为内容协商器处理器
      * HeaderContentNegotiationStrategy
      */
            for (ContentNegotiationStrategy strategy : this.strategies) {
                List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
                if (mediaTypes.isEmpty() || mediaTypes.equals(MEDIA_TYPE_ALL)) {
                    continue;
                }
                return mediaTypes;
            }
            return Collections.emptyList();
        }
    
    image

    由于这个内容协商处理器在第一位他会被执行,这个处理器根据请求地址的后缀也默认一些返回的content-type类型,比如默认的json->application/json;xml->application/xml等等,按理来说均无法匹配,但是它后边有个调用容器this.servletContext.getMimeType("file." + extension)方法(extension为htm),竟然返回了text\html,然后他就把这个当成自己的常用匹配并且把htm->text\html加入了默认的集合,这也是网上一些人说spring会根据后缀名猜返回值类型的出错,其实是servletContext.getMimeType的问题
    由于对象的处理的jackson也就是MappingJackson2HttpMessageConverter,他返回支持的类型是application/json,这就造成了请求的类型为text/html,可处理的类型为application/json无法匹配,报错
    但是可以发现HeaderContentNegotiationStrategy处理类还是根据请求头的accept来判断的,

    3 解决

    • ServletPathExtensionContentNegotiationStrategy这个处理器干掉
    • 注册一个既能处理对象返回结果(application/json),又能返回支持text/html方式的返回值处理器

    第一种方法:

    <mvc:annotation-driven content-negotiation-manager="contentNegotiationManager" />
        <bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
            <property name="useJaf" value="false"/>
            <!--干掉路径扩展 也就是ServletPathExtensionContentNegotiationStrategy-->
            <property name="favorPathExtension" value="false"/>
    
        </bean>
    

    所有的自定义标签均是AnnotationDrivenBeanDefinitionParser类解析,进入spring-mvc包的AnnotationDrivenBeanDefinitionParser
    进入 parse方法

    @Override
        public BeanDefinition parse(Element element, ParserContext parserContext) {
        ...
        // 构造内容协商
        RuntimeBeanReference contentNegotiationManager = 
        getContentNegotiationManager(element, source, parserContext);
        ...
    }
    
    private RuntimeBeanReference getContentNegotiationManager(Element element, Object source,
                ParserContext parserContext) {
    
            RuntimeBeanReference beanRef;
            if (element.hasAttribute("content-negotiation-manager")) {
                String name = element.getAttribute("content-negotiation-manager");
                beanRef = new RuntimeBeanReference(name);
            }
            else {
                RootBeanDefinition factoryBeanDef = new RootBeanDefinition(ContentNegotiationManagerFactoryBean.class);
                factoryBeanDef.setSource(source);
                factoryBeanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
                factoryBeanDef.getPropertyValues().add("mediaTypes", getDefaultMediaTypes());
    
                String name = CONTENT_NEGOTIATION_MANAGER_BEAN_NAME;
                parserContext.getReaderContext().getRegistry().registerBeanDefinition(name , factoryBeanDef);
                parserContext.registerComponent(new BeanComponentDefinition(factoryBeanDef, name));
                beanRef = new RuntimeBeanReference(name);
            }
            return beanRef;
        }
    

    可以发现如果不制定content-negotiation-manager那么就会以ContentNegotiationManagerFactoryBean类默认属性来构造

    Override
        public void afterPropertiesSet() {
            List<ContentNegotiationStrategy> strategies = new ArrayList<ContentNegotiationStrategy>();
    
            if (this.favorPathExtension) {
                PathExtensionContentNegotiationStrategy strategy;
                if (this.servletContext != null && !isUseJafTurnedOff()) {
                    strategy = new ServletPathExtensionContentNegotiationStrategy(
                            this.servletContext, this.mediaTypes);
                }
                else {
                    strategy = new PathExtensionContentNegotiationStrategy(this.mediaTypes);
                }
                strategy.setIgnoreUnknownExtensions(this.ignoreUnknownPathExtensions);
                if (this.useJaf != null) {
                    strategy.setUseJaf(this.useJaf);
                }
                strategies.add(strategy);
            }
    
            if (this.favorParameter) {
                ParameterContentNegotiationStrategy strategy =
                        new ParameterContentNegotiationStrategy(this.mediaTypes);
                strategy.setParameterName(this.parameterName);
                strategies.add(strategy);
            }
    
            if (!this.ignoreAcceptHeader) {
                strategies.add(new HeaderContentNegotiationStrategy());
            }
    
            if (this.defaultNegotiationStrategy != null) {
                strategies.add(this.defaultNegotiationStrategy);
            }
    
            this.contentNegotiationManager = new ContentNegotiationManager(strategies);
        }
    

    ContentNegotiationManagerFactoryBean类的afterPropertiesSet()方法可以看到
    如果favorPathExtension属性为true(默认为true)时就会根据是否使用Jaf来判断是否构造ServletPathExtensionContentNegotiationStrategy或者PathExtensionContentNegotiationStrategy(和文件有关),所以我们主动声明favorPathExtensionfalse可以禁止注册此处理器

    关于内容协商有个很好的文章:https://blog.csdn.net/u012410733/article/details/78536656

    第二种方法:

    <bean
            class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
            <property name="messageConverters">
                <list>
                    <bean
                        class="org.springframework.http.converter.StringHttpMessageConverter">
                        <property name="supportedMediaTypes">
                            <list>
                                <value>text/html;charset=UTF-8</value>
                            </list>
                        </property>
                    </bean>
                    <bean 
                        class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter">  
                        <property name="supportedMediaTypes">  
                            <list>  
                                <value>text/html;charset=UTF-8</value>  
                            </list>  
                        </property>  
                    </bean>  
                </list>
            </property>
        </bean>
    
    

    相关文章

      网友评论

          本文标题:spring升级后Ajax请求出错(406 Not Accept

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