今天通过spring mvc3.2.0 中@ResponseBody对ajax请求进行返回json数据的时候,后台提示:
ExceptionHandlerExceptionResolver - Resolving exception from handler [......]org.springframework.web.HttpMediaTypeNotAcceptableException : Could not find acceptable representation
firebug显示:
浏览器向Controller中传递的是参数:aaaaa@aaa.com.au会报这种异常; 但是如果是参数aaaaa@aaa.com则不会报异常。
想着问题肯定出在‘au’上,au这个玩意儿很像一种媒体类型,暂时定位到spring接收参数后进行json数据格式包装是出错了,包装的时候搞成其他类型(非json)。于是debug跟踪spring mvc源码:
根据问题定位,跟踪到这里:
AbstractMessageConverterMethodProcessor类的writeWithMessageConverters方法:
/**
* Writes the given return value to the given web request. Delegates to
* {@link #writeWithMessageConverters(Object, MethodParameter, ServletServerHttpRequest, ServletServerHttpResponse)}
*/
protected <T> void writeWithMessageConverters(T returnValue,MethodParameter returnType,NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException {
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
接着调用本类的writeWithMessageConverters方法:
protected <T> void writeWithMessageConverters (T returnValue,MethodParameter returnType,ServletServerHttpRequest inputMessage,ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException {
Class<?> returnValueClass = returnValue.getClass();
HttpServletRequest servletRequest = inputMessage.getServletRequest();
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(servletRequest);
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(servletRequest, returnValueClass);
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
for (MediaType r : requestedMediaTypes) {
for (MediaType p : producibleMediaTypes) {
if (r.isCompatibleWith(p)) {
compatibleMediaTypes.add(getMostSpecificMediaType(r, p));
}
}
}
if (compatibleMediaTypes.isEmpty()) {
throw new HttpMediaTypeNotAcceptableException(allSupportedMediaTypes );
}
......
}
throw new HttpMediaTypeNotAcceptableException(allSupportedMediaTypes );
}
跟踪getAcceptableMediaTypes方法:
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;
}
调用ContentNegotiationManager类的resolveMediaTypes方法获得mediatype:
public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException {
for (ContentNegotiationStrategy strategy : this. contentNegotiationStrategies) {
List<MediaType> mediaTypes = strategy.resolveMediaTypes(webRequest);
if (!mediaTypes.isEmpty()) {
return mediaTypes;
}
}
return Collections. emptyList();
}
而接口ContentNegotiationStrategy有很多实现,明显的策略模式,要拿到这个mediaTypes有几个策略:
然而根据相应策略到了类AbstractMappingContentNegotiationStrategy的resolveMediaTypes方法:
public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) {
String key = getMediaTypeKey(webRequest);
if (StringUtils. hasText(key)) {
MediaType mediaType = lookupMediaType(key);
if (mediaType != null) {
handleMatch(key, mediaType);
return Collections. singletonList(mediaType);
}
mediaType = handleNoMatch(webRequest, key);
if (mediaType != null) {
addMapping(key, mediaType);
return Collections. singletonList(mediaType);
}
}
return Collections. emptyList();
}
重点来了,首先是根据webRequest获取key,key是啥呢?跟踪到 getMediaTypeKey(webRequest)方法内:
跟踪到类MappingMediaTypeFileExtensionResolver的getMediaTypeKey方法:
@Override
protected String getMediaTypeKey(NativeWebRequest webRequest) {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class );
if (servletRequest == null) {
logger.warn( "An HttpServletRequest is required to determine the media type key");
return null;
}
String path = urlPathHelper.getLookupPathForRequest(servletRequest);
String filename = WebUtils.extractFullFilenameFromUrlPath(path);
String extension = StringUtils. getFilenameExtension(filename);
return (StringUtils. hasText(extension)) ? extension.toLowerCase(Locale.ENGLISH ) : null;
}
原来key是获取参数值的扩展名,也就是.au.获取来了做什么呢?
之前 resolveMediaTypes方法中的lookupMediaType,handleNoMatch方法,根据key(au)去找spring中自带的媒体类型。
不幸的是au正是其中的媒体类型:
拿到了mediaType,我们回到之前AbstractMessageConverterMethodProcessor类的writeWithMessageConverters方法:
其中requestedMediaTypes的类型是[audio/basic],producibleMediaTypes的类型是[application/json;charset=UTF-8, application/*+json;charset=UTF-8, application/json;charset=UTF-8, application/*+json;charset=UTF-8],两个类型不符,固然进入:
if (compatibleMediaTypes.isEmpty()) {
throw new HttpMediaTypeNotAcceptableException(allSupportedMediaTypes );
}
抛出了异常。
那我们怎么解决呢?
spring 提供了这么一个类:org.springframework.web.accept.ContentNegotiationManagerFactoryBean。
帮助我们选择自己想要的 media types策略。
/**
* A factory providing convenient access to a {@code ContentNegotiationManager}
* configured with one or more {@link ContentNegotiationStrategy} instances.
*
* <p>By default strategies for checking the extension of the request path and
* the {@code Accept} header are registered. The path extension check will perform
* lookups through the {@link ServletContext} and the Java Activation Framework
* (if present) unless {@linkplain #setMediaTypes(Properties) media types} are configured.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
public class ContentNegotiationManagerFactoryBean
implements FactoryBean<ContentNegotiationManager>, InitializingBean, ServletContextAware {
private boolean favorPathExtension = true;
private boolean favorParameter = false;
private boolean ignoreAcceptHeader= false;
private Properties mediaTypes = new Properties();
private Boolean useJaf;
private String parameterName;
private MediaType defaultContentType;
private ContentNegotiationManager contentNegotiationManager;
private ServletContext servletContext;
......
我们暂且看看这个类的属性就知道他是做什么的了。
favorPathExtension 是否根据request path的扩展名进行选择mediaType。
favorParameter 是否根据参数选择medieType。
ignoreAcceptHeader 是否忽略浏览器传过来的acceptHeader。
mediaTypes 媒体类型。
那么顺其自然,我们在spring mvc配置文件中servlet-context.xml中这样配置即可解决:
<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager"/>
< beans:bean id= "contentNegotiationManager" class= "org.springframework.web.accept.ContentNegotiationManagerFactoryBean" >
<beans:property name ="favorPathExtension" value= "false" />
<beans:property name ="favorParameter" value="false" />
<beans:property name ="ignoreAcceptHeader" value= "false" />
<beans:property name ="mediaTypes" >
<beans:value >
atom=application/atom+ xml
html=text/html
json=application/json
*=*/*
</beans:value >
</beans:property >
</beans:bean>
这样直接根据http的acceptHeader的策略进行选择mediaTypes。问题解决。
另外在解决问题的过程中,看到说这是spring mvc 3.2.0的一个bug,可以将版本降到3.1即可解决。
还有一种解决方法:参考http://stackoverflow.com/questions/14333709/spring-mvc-3-2-and-json-objectmapper-issue。
这种.au的参数让我碰到了这种问题,也是人品啊。