问题
在公司接手了个项目,dispatchServlet奇葩的监听了*.htm结尾的请求。

这将导致请求头中即使加了"Accept", “application/json”,并且Controller中也加了@ResponseBody的情况下,该接口也会报 org.apache.http.client.HttpResponseException: Not Acceptable 异常
请求端代码

接受端代码

抛出异常

解决过程
Debug 找问题
首先定位到异常是下面的这个的方法抛出的
ServletInvocableHandlerMethod
mavContainer.setRequestHandled(false);
try {
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
然后继续debug
发现spring会选择请求结果的处理器。
继续debug
发现spring 使用RequestResponseBodyMethodProcessor这个类来处理该返回

进入了AbstractMessageConverterMethodProcessor并继续debug
发现是因为compatibleMediaTypes这个集合为空导致的报错
compatibleMediaTypes:适配的媒体类型

继续探究compatibleMediaTypes为啥会是空。

会发现是这两个requestedMediaTypes和producibleMediaTypes不匹配导致的
先来看看这两个里面是什么

首先看看producibleMediaTypes的值是如何获取的。进入getProducibleMediaTypes方法

先来看看有哪些 convertor

可以看到有7个转换器
但是只有这个MappingJackson2HttpMessageConverter这个转换器能处理返回结果。话说这个转换器 看起来是不是很眼熟啊。他能处理的mediaType是application/json和application/*+json这两种类型。

再来看看requestedMediaTypes是怎么获取的。进入getAcceptableMediaTypes方法
发现是通过ContentNegotiationManager的resolveMediaTypes方法获取的
ContentNegotiationManager有两个策略(这两个策略的先后顺序是导致这个问题的原因)


进入ContentNegotiationManager的resolveMediaTypes方法可见

发现ContentNegotiationManager会调用内置的两个策略进行匹配
关键的关键,他是一个策略匹配上了则直接返回了,并且优先使用ServletPathExtensionContentNegotiationStrategy策略
进入该策略的resolveMediaTypes方法

getMediaTypeKey(webRequest)这个方法其实就是截取后缀,这里就不细看了。
下图可见 getMediaTypeKey(webRequest)获取的值是htm

继续debug 可见该策略根据请求后缀决定了requestedMediaTypes的值

再来看看HeaderContentNegotiationStrategy策略的resolveMediaTypes方法
原来是这个策略才是从请求头中获取ACCEPT。那么要是这个策略优先问题不就解决了。

综上概括下问题原因
发生这个异常是因为请求结果的RequestResponseBodyMethodProcessor处理器的ContentNegotiationManager策略管理器优先使用路径匹配规则,也就是以htm为后缀的请求默认使用text/html类型的转换器,该类型与转换器可处理的application/json和application/*+json类型不相符导致的跑异常。
那么如果优先使用HeaderContentNegotiationStrategy也就是请求头中的ACCEPT优先的话问题就解决了。
解决办法
继续研究contentNegotiationManager是如何生成的。之后找到解决办法
项目启动后获取容器中的RequestMappingHandlerAdapter对象。之后通过反射获取RequestMappingHandlerAdapter中的contentNegotiationManager对象,再获取contentNegotiationManager 中List 策略集合 。之后翻转该集合即可。
代码如下
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.ContentNegotiationStrategy;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.List;
/**
- by wangxingpeng
/
@Component
public class ContextRefreshedEventListener implements ApplicationListener {
/*- 解決以htm结尾的请求,加@ResponseBody返回406的问题
- @param event
*/
@Override
public void onApplicationEvent(final ContextRefreshedEvent event) {
ApplicationContext applicationContext = (ApplicationContext) event.getSource();
if (!applicationContext.containsBean(RequestMappingHandlerAdapter.class.getName())) {
return;
}
RequestMappingHandlerAdapter requestMappingHandlerAdapter = applicationContext.getBean(RequestMappingHandlerAdapter.class);
Class c = RequestMappingHandlerAdapter.class;
try {
Field f = c.getDeclaredField(“contentNegotiationManager”);
f.setAccessible(true);
ContentNegotiationManager contentNegotiationManager = (ContentNegotiationManager) f.get(requestMappingHandlerAdapter);
List list = contentNegotiationManager.getStrategies();
Collections.reverse(list);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
问题解决成功返回


被折叠的 条评论
为什么被折叠?



