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

1.背景

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

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中

由于我们是有注解@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  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 requestedMediaTypes = getAcceptableMediaTypes(request);
		// 可处理返回值类型的处理器可以接受的返回值类型
		List producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);

		if (outputValue != null && producibleMediaTypes.isEmpty()) {
			throw new IllegalArgumentException("No converter found for return value of type: " + valueType);
		}

		Set compatibleMediaTypes = new LinkedHashSet();
		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 getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
		List mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
		return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes);
	}
复制代码
@Override
	public List resolveMediaTypes(NativeWebRequest request)
			throws HttpMediaTypeNotAcceptableException {

/**
  * strategies 注册了两个处理器
  * ServletPathExtensionContentNegotiationStrategy即为内容协商器处理器
  * HeaderContentNegotiationStrategy
  */
		for (ContentNegotiationStrategy strategy : this.strategies) {
			List mediaTypes = strategy.resolveMediaTypes(request);
			if (mediaTypes.isEmpty() || mediaTypes.equals(MEDIA_TYPE_ALL)) {
				continue;
			}
			return mediaTypes;
		}
		return Collections.emptyList();
	}
复制代码

由于这个内容协商处理器在第一位他会被执行,这个处理器根据请求地址的后缀也默认一些返回的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方式的返回值处理器

第一种方法:

"contentNegotiationManager" />
	"contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
		"useJaf" value="false"/>
		
		"favorPathExtension" value="false"/>

	
复制代码

所有的自定义标签均是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 strategies = new ArrayList();

		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可以禁止注册此处理器

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

第二种方法:

"org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
		"messageConverters">
			
				"org.springframework.http.converter.StringHttpMessageConverter">
					"supportedMediaTypes">
						
							text/html;charset=UTF-8
						
					
				
				"org.springframework.http.converter.json.MappingJacksonHttpMessageConverter">  
				    "supportedMediaTypes">  
				          
				            text/html;charset=UTF-8  
				          
				      
				  
			
		
	

复制代码

猜你喜欢

转载自juejin.im/post/5d821e3a6fb9a06b122f7273