Ajax spring after the upgrade request error (406 Not Acceptable)

1. Background

As business needs, today's JDKupgrade to 1.8the container requirements Springalso need to upgrade to 4.0+later, to solve the problem completely dependent code starts successfully, the page display normal, but encountered Ajaxlocal requests on the bombing, error code 406, request failed, content can not return to normal, Debugfound that business code processing logic to perform normal, suspected in Springrendering the results of error F12analysis can be found in the request header contents are not returned application/jsonbut text\htmldoes not meet the @ResponseBodypurpose of annotations.

2. Analysis

First to enter the DispatcherServletclass of doDispatchcore processing

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

	.....

}
复制代码

AbstractHandlerMethodAdapter.handleMethod calls the abstract method handleInternal, we return to a subclass RequestMappingHandlerAdapterof View

@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;
	}
复制代码

Can be found in any case need to take invokeHandlerMethod(request, response, handlerMethod)this approach, this is what we need to keep track approach

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);The logic here is the main processing side includes processing request, and return values ​​decor

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);
	}
复制代码

Register value processor is returned from the front right of the selected processor and processes the request, debug register of the processor 15 found in

Since we are annotated @ResponseBody, our processor isRequestResponseBodyMethodProcessor

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()The acquisition request of content-typethe type 3.0 and 4.0 there is a big difference, 3.0 directly by requesting the acquisition head, and 4.0 undergone 内容协商器the processor, the processor is ``

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();
	}
复制代码

Since this content negotiation processor he will be executed in the first place, this processor is also based on some of the default return address suffix request content-typetype, such as the default json->application/json;xml->application/xml, and so, logically, it was unable to match, but behind it there is a call to container this.servletContext.getMimeType("file." + extension)method (extension is htm), unexpectedly returned text\html, and he took this as his usual match and the htm->text\htmladdition of the default collection, which is the internet guess some people would say spring return type of error according to the suffix, in fact, is servletContext.getMimeTypethe problem because the object of treatment jacksonis MappingJackson2HttpMessageConverter, his return types are supported application/json, which resulted in the request for the type text/html, the type that can be processed to application/jsonnot match, an error but can be found in HeaderContentNegotiationStrategyprocessed or at the request of the head of the class acceptto judge,

3 to solve

  • Will ServletPathExtensionContentNegotiationStrategythis get rid processor
  • Both object returns a registration result of the processing (application / json), but returns to support text/htmlembodiment of the return value processor

the first method:

<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>
复制代码

All custom labels are AnnotationDrivenBeanDefinitionParserbased resolved into the spring-mvcpacket AnnotationDrivenBeanDefinitionParsertype into the parsemethod of

@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;
	}
复制代码

It can be found if you do not develop content-negotiation-managerit will to ContentNegotiationManagerFactoryBeanclass default property to construct

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);
	}
复制代码

In the ContentNegotiationManagerFactoryBeanclass afterPropertiesSet()if the method can be seen favorPathExtensionproperty true(default is true) depending on whether it will use Jafto determine whether construction ServletPathExtensionContentNegotiationStrategyor PathExtensionContentNegotiationStrategy(and related documents), so we take the initiative to declare favorPathExtensionas falsemay prohibit registered this processor

About Content Negotiation has a very good article: blog.csdn.net/u012410733/...

The second method:

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

复制代码

Guess you like

Origin juejin.im/post/5d821e3a6fb9a06b122f7273