spring升級後Ajax請求出錯(406 Not Acceptable)

1.背景

因爲業務須要,今天公司的JDK升級到1.8,容器要求Spring也須要同時升級到4.0+,解決完依賴的問題以後,代碼啓動成功,頁面展現正常,可是遇到Ajax請求的地方就炸了,錯誤碼406,致使請求失敗,內容沒法正常返回,Debug發現業務代碼處理邏輯執行正常,懷疑在Spring對結果的渲染出錯,F12分析請求能夠發現返回頭的內容內容並非application/json而是text\html,不符合@ResponseBody註解的目的。html

2.分析

首先進入DispatcherServlet類的doDispatch核心處理web

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中查看spring

@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)這個方法,這個也就是咱們須要跟蹤的方法json

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);是主要的處理邏輯這裏邊包含了請求的處理,和返回值的裝飾spring-mvc

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

因爲咱們是有註解@ResponseBody,咱們的處理器就是RequestResponseBodyMethodProcessorsession

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經歷了內容協商器這個處理器,這個處理器就是 ``mvc

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();
	}
複製代碼

因爲這個內容協商處理器在第一位他會被執行,這個處理器根據請求地址的後綴也默認一些返回的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來判斷的,app

3 解決

  • ServletPathExtensionContentNegotiationStrategy這個處理器幹掉
  • 註冊一個既能處理對象返回結果(application/json),又能返回支持text/html方式的返回值處理器

第一種方法:async

<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能夠禁止註冊此處理器

關於內容協商有個很好的文章:blog.csdn.net/u012410733/…

第二種方法:

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

複製代碼
相關文章
相關標籤/搜索