spring升级后Ajax请求出错(406 Not Accept
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);
}
image从前边注册的返回值处理器中选择正确的处理器并处理请求,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 <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
(和文件有关),所以我们主动声明favorPathExtension
为false
可以禁止注册此处理器
关于内容协商有个很好的文章: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>