欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

基于SpringMvc实现Restful方式接口返回406的问题

程序员文章站 2024-01-10 19:00:52
...

一、问题:

生产环境终端请求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