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

2019-10-12  本文已影响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 解决

第一种方法:

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

上一篇 下一篇

猜你喜欢

热点阅读