基于SpringMvc实现Restful方式接口返回406的问题
一、问题:
生产环境终端请求http://ip:port/service/cn.com.otg 返回406
二、问题排查:
本地请求http://ip:port/service/cn.com.otg 返回406
HTTP Status 406 -
type Status report
message
description The resource identified by this request is only capable of generating responses with characteristics not acceptable according to the request "accept" headers.
本地请求http://ip:port/service/cn.otg 返回406
HTTP Status 406 -
type Status report
message
description The resource identified by this request is only capable of generating responses with characteristics not acceptable according to the request "accept" headers.
本地请求http://ip:port/service/cn.otg.aaa 正常返回
本地请求http://ip:port/service/cn.com.otg2 正常返回
本地请求http://ip:port/service/cn.com.aaa 正常返回
额,问题定位,不能以otg结尾!
为什么呢?
根据提示
description The resource identified by this request is only capable of generating responses with characteristics not acceptable according to the request "accept" headers.
服务响应不接受请求的accept。这又是为什么呢?request请求header里的accept是什么?如下:
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
以上五个请求都是这个。那为什么前两个不接受,后三个就接受了呢?
其实前两个请求经过spring处理以后就不再按照浏览器或者客户端请求头里面带有的accept做处理了。本来以为4XX开头的都是客户端的问题,跟调用接口的同事还墨迹了一番,后来本地调试居然进到的服务端的处理方法里面了,于是服务器端排查。
首先找到处理@ResponseBody的处理器
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor,定位到写json输出的方法,debug:
protected <T> void writeWithMessageConverters(T returnValue, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
Class<?> returnValueClass = getReturnValueType(returnValue, returnType);
Type returnValueType = getGenericType(returnType);
HttpServletRequest servletRequest = inputMessage.getServletRequest();
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(servletRequest);
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(servletRequest, returnValueClass, returnValueType);
if (returnValue != null && producibleMediaTypes.isEmpty()) {
throw new IllegalArgumentException("No converter found for return value of type: " + returnValueClass);
}
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 (returnValue != null) {
throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
}
return;
}
getAcceptableMediaTypes和getProducibleMediaTypes就是我们这边涉及到的accept和pruduce相关的,debug到getAcceptableMediaTypes发现requestedMediaTypes变成了application/vnd.oasis.opendocument.graphics-template,这个是个媒体类型,为什么accept里面会是这个媒体类型呢?
跟踪方法内部,到达org.springframework.web.accept.ContentNegotiationManager类的resolveMediaTypes方法:
@Override
public List<MediaType> resolveMediaTypes(NativeWebRequest request)
throws HttpMediaTypeNotAcceptableException {
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();
}
发现这边spring根据策略模式实现了以下六种:
AbstractMappingContentNegotiationStrategy、FixedContentNegotiationStrategy、HeaderContentNegotiationStrategy、
还有三种ParameterContentNegotiationStrategy、PathExtensionContentNegotiationStrategy继承AbstractMappingContentNegotiationStrategy,
ServletPathExtensionContentNegotiationStrategy继承FixedContentNegotiationStrategy。
跟进resolveMediaTypes方法
@Override
public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest)
throws HttpMediaTypeNotAcceptableException {
return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest));
}
@Override
protected String getMediaTypeKey(NativeWebRequest webRequest) {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request == null) {
logger.warn("An HttpServletRequest is required to determine the media type key");
return null;
}
String path = PATH_HELPER.getLookupPathForRequest(request);
String filename = WebUtils.extractFullFilenameFromUrlPath(path);
String extension = StringUtils.getFilenameExtension(filename);
return (StringUtils.hasText(extension)) ? extension.toLowerCase(Locale.ENGLISH) : null;
}
String extension = StringUtils.getFilenameExtension(filename);
这边会根据url中的以“.**”结尾的请求获取到扩展名,然后根据org.springframework.web.accept.MappingMediaTypeFileExtensionResolver类的lookupMediaType方法,找到扩展名对应的媒体类型
/**
* Use this method for a reverse lookup from extension to MediaType.
* @return a MediaType for the key, or {@code null} if none found
*/
protected MediaType lookupMediaType(String extension) {
return this.mediaTypes.get(extension.toLowerCase(Locale.ENGLISH));
}
这样拿到requestedMediaTypes,再回到@ResponseBody注解处理器里面,一一遍历requestedMediaTypes和producibleMediaTypes,如果producibleMediaTypes的集合没有匹配到requestedMediaTypes中的数据,就报了description The resource identified by this request is only capable of generating responses with characteristics not acceptable according to the request "accept" headers.
其实我认为这里面的提示有点不是很直观,提示描述是从header里面获取accept不接受,第一感觉就是客户端代码或者浏览器设置的header头里面的accept。
三、解决过程:
1、尝试过通过filter去改变header的值,改变不了,只能覆盖getHeader方法
2、考虑过继承策略类重写protected abstract String getMediaTypeKey(NativeWebRequest request);方法,还好没有这么做,估计会引发其他问题
四、最终解决方法:
<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager"></mvc:annotation-driven>
<bean id= "contentNegotiationManager" class= "org.springframework.web.accept.ContentNegotiationManagerFactoryBean" >
<property name ="favorPathExtension" value= "false" />
</bean>
将favorPathExtension设置为false
五、总结:
Restful风格的接口设计,尽量不要把url的最后一个"/"后面设计成参数,即@PathVariable形式接收的参数
不建议的设计:
http://ip:port/xxx/xxxx/{ppp}
建议的设计:
http://ip:port/xxx/{ppp}/xxxx
因为ppp一旦是转义型的字符或者以媒体类型结尾的后缀,需要做一些处理。
(基于springboot做微服务涉及到类似的问题可以参考着处理)
转载于:https://my.oschina.net/anis/blog/1587778
上一篇: php设计模式之单例、多例设计模式
下一篇: 推荐10款用得着源码(收藏)