IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> 记一次SpringMvc下HTTP 406问题排查 -> 正文阅读

[网络协议]记一次SpringMvc下HTTP 406问题排查

问题背景

由于项目需要,需要将某个SpringMvc的Rest接口响应修改为json类型,结果发现原来正常的请求会报HTTP 406,这里记录一下追踪的过程。

先简单介绍一下HTTP 406。

HTTP 406 (Not Acceptable)

The requested resource is only capable of generating content not acceptable according to the Accept headers sent in the request.

Accept

Accept代表发送端(客户端)希望接收的数据类型,*/*表示可以接收任何类型。

Content-Type

代表响应端(服务器)发送的实体数据的数据类型

如果双方不一致,也就是说客户端请求的accept和服务端响应的content-type不兼容,就会出现前面提到的406错误。

问题复现

原有接口示例
@RequestMapping(value = "/hello/**")
@ResponseBody
public String helloWorld(HttpServletRequest httpServletRequest) {
  return "hello world";
}

客户端对应的请求连接

localhost:8080/hello/test.htm

使用postman模拟请求,可以看到请求时的Accept是*/*,而服务端返回的Content-Type是text/html。

image-20211115132848248
接口改造

按照业务需求,需要将响应统一修改为appication/json类型,对于SpringMvc的Rest请求,我们做了如下修改,增加produces标识响应类型为application/json。

关于produces属性的含义

Narrows the primary mapping by media types that can be produced by the mapped handler(限制该方法的MediaType)

// see MediaType.java
// public static final String APPLICATION_JSON_VALUE = "application/json";
@RequestMapping(value = "/hello/**", produces = {MediaType.APPLICATION_JSON_VALUE})
@ResponseBody
public String helloWorld(HttpServletRequest httpServletRequest) {
  return "hello world";
}

改造后重新使用Postman测试,发现响应的Content-Type虽然变成了application/json类型,但是出现了HTTP 406。如果将请求uri中的htm后缀去掉后,请求就变为正常了。

image-20211115134407729 image-20211115134633093

显然,问题出现在请求后缀上,需要进一步排查问题原因。

对于SpringMvc的请求过程,首先需要DispatchServlet根据请求HttpServletRequest,利用HandlerMapping获取到对应的HandlerChain。

获取HandlerChain

这里对于采用了@RequestMapping注解的方法,会使用RequestMappingHandlerMapping方法。

image-20211115135844714

当然,其中有些方法会存在于父类AbstractHandlerMethodMapping中,我们断点到lookupHandlerMethod方法。

可以看到当前请求的类就是RequestMappingHandlerMapping,它的mappingRegistry中包含了我们的请求接口"/hello/**",对应的Produces是application/json类型。

image-20211115140626302

然后会进入到该类的addMatchingMappings方法中,寻找满足条件的mapping信息。

image-20211115140941065

继续进入getMatchingMappings方法,最终会进入到RequestMappingInfo的getMatchingCondition方法。这个方法会对请求中的很多属性进行校验,包括请求方法、参数、header,consumers以及produces,这里我们重点关注producesCondition的getMatchingCondition方法,通过后续的分析也可以得到,这是出问题的根本所在。

image-20211115141730032

关于这个方法,可以先看一下javaDoc的注释。

Checks if any of the contained media type expressions match the given request ‘Content-Type’ header and returns an instance that is guaranteed to contain matching expressions only.

方法内部会先根据request获取到acceptedMediaTypes,即getAcceptedMediaTypes方法。然后将获取到的同当前produces提供的进行匹配。

image-20211115142056568
获取请求的acceptedMediaTypes

在getAccepedMediaTypes内部会调用核心的ContentNegotiationManager解析请求的MediaTypes,这个类中会注册一些ContentNegotiationStrategy。在当前断点条件下有HeaderContentNegotiationStrategy和ServletPathExtensionNegotiationStrategy。

image-20211115143039199

我们进入到该方法内部,会循环遍历所有的Strategy解析到MediaTypes。

image-20211115143341902

首先会进入ServletPathExtensionNegotiationStrategy的解析,会先进入到父类中的resolveMediaTypes方法。

image-20211115143709976

注意到上面的getMediaTypeKey方法,该方法是一个抽象方法,拥有两个实现。

image-20211115143858546

当前情况下会进入PathExtensionContentNegotiationStrategy中

image-20211115144016948

这里会返回htm,然后进入到前面的AbstractMappingContentNegotiationStrategy的resolveMediaTypeKey方法。

image-20211115144445418

其中lookupMediaType位于MappingMediaTypeFileExtensionResolver中。

	@Nullable
	protected MediaType lookupMediaType(String extension) {
		return this.mediaTypes.get(extension.toLowerCase(Locale.ENGLISH));
	}
image-20211115144607135

该方法的MediaType中只有xml和json,所以对于htm返回空。进而会进入到AbstractMappingContentNegotiationStrategy的handleNoMatch方法,这里会进入到ServletPathExtensionContentNegotiationStrategy的handleNoMatch方法中。它会根据文件后缀,得到MediaType为text/html类型。

image-20211115145130802
不匹配情况下抛出HttpMediaTypeNotAcceptableException

现在我们可以回到ProducesCondition方法中,获取到acceptMediaType后,和Produces的进行匹配,进入到getMatchingExpressions方法。可以看到当前类的expression是application/json,但是accepted是text/html,这里会返回空。进一步,produceCondition会返回null。

image-20211115145913855

继续向上返回RequestMapping.getMatchingCondition也会返回空。

image-20211115150145816

继续返回,会回到AbstractHandlerMethodMapping的lookupHandlerMethod方法

image-20211115150724820

由于上述的matches是空的,所以方法会执行到handleNoMatch方法,该方法是抽象方法。RequestMappingInfoHandlerMapping对该方法进行了重写。方法开始的PartialMathHelper初始化的时候,会对各种Condition进行校验,可以看到这里又执行了一遍之前的getMatchingCondition方法,并且同理producesMatch的结果是false。而我们看到在第267行,如果有produces不匹配的情况下,就会抛出HttpMediaTypeNotAcceptableException异常。

image-20211115151156323

image-20211115151511704

到这里问题已经基本明确了,那么对于原始的,没有添加produces属性的接口,为什么是可以的呢?

我们可以直接定位到ProducesRequesetCondition,直接debug到getMatchingCondition,可以看到它的expression是空的,isEmpty如果发现expression是空的,不会对accept的contentType做校验,后续也就不会抛出HttpMediaTypeNotAcceptableException异常了。

image-20211115152941901

解决办法

针对这种情况,目前最好的解决方法是禁掉根据后缀类型匹配MediaType。

image-20211115154715735

该配置可以通过查看ContentNegotiationManagerFactoryBean这个类中的favorPathExtension属性。

在Spring-webmvc的5.3.5中,该配置是默认关闭的。

image-20211115155606875

但是在4.3.16中,该配置是开启的。

image-20211115160306685

关闭配置的方法

@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.favorPathExtension(false);
        super.configureContentNegotiation(configurer);
    }
}

问题总结

在本次排查过程中,有几点需要注意。

  1. 不同版本的Spring ContentNegotiation配置存在差异。

  2. 在前面提到的MappingMediaTypeFileExtensionResolverd的lookupMediaType中,存在mediaTypes取值为application/json和application/xml,他们的来源在哪里呢?

    这里可以查看WebMvcConfigurationSupport中的getDefaultMediaTypes,可以看到这里会根据一些变量做一些初始化的工作。

    image-20211115164726570

? 而上述变量的取值情况如下,也就是会根据classpath中的类情况做初始化工作

image-20211115164831090

问题延伸

还有一种情况接口返回HTTP 406的情况,这种会出现在使用到了HttpMessageConverter时。

接口会返回对象,如以下case

@RequestMapping(value = "/listPerson")
@ResponseBody
public List<Person> listPerson() {
  Person person = new Person();
  person.setId(1L);
  person.setName("zhangsan");
  return Lists.newArrayList(person);
}

具体可以跟进到AbstractMessageConverterMethodProcessor的writeWithMessageConverters方法中。

image-20211115171207241

首先可以看到body中是有正常值的,上图中的逻辑和之前有些类似,218行先根据request获取到acceptedMediaType,最终也会调用this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));

然后再获取produceMediaType即230行代码,然后在237行到241行,进行匹配。如果匹配失败,则会在246行抛出HttpMediaTypeNotAcceptableException。

这里再着重看一下getProducibleMediaTypes方法。

主要就是根据HttpMessageConverter的canWrite方法判断是否可以对返回结果进行write,可以的话,添加getSupportedMediaTypes即可。

image-20211115172003721

image-20211115172048790

这里有很多的HttpMessageConverter,最终会利用MappingJackson2HttpMessageConvertoer增加applicaiton/json和applicaiton*+json。

而它的getSupportedMediaTypes会进入AbstractJackson2HttpMessageConverter中,如果有自定义的objectMapper,那就使用自定义的。

image-20211115172539003

否则的话,调用AbstractHttpMessageConverter的getSupportedMediaTypes方法。

image-20211115172742548

? 还是需要看一下this.supportedMediaTypes的来源。

? 可以直接看一下MappingJackson2HttpMessageConverter的初始化函数,终于找到你。

? image-20211115173309240

? 所以,如果在这种情况下,客户端的请求accept如果是application/xml,也会返回HTTP 406。

? image-20211115173449827

? 最后,如果真要返回application/xml,怎么办呢? 还是需要看一下完成的WebMvcConfigurationSupport类。

? 这次的方法是addDefaultHttpMessageConverters,添加默认的messageConverter(代码有些长,截取了前半部分)。

? image-20211115173838337

? 可以看到xml解析的条件是!shoudIgnoreXml,该值默认是false,那另外一个条件就是jackson2XmlPresent。是的,这个配置在第二个关注点中有描述,即

? image-20211115174226455
? 所以需要先添加maven依赖

<dependency>
  <groupId>com.fasterxml.jackson.dataformat</groupId>
  <artifactId>jackson-dataformat-xml</artifactId>
  <version>2.11.4</version>
</dependency>

? 此时AbstractMessageConverterMethodProcessor的getProducibleMediaTypes终于看到了xml类型啦。

? image-20211115174641587

再用Postman试一下,大功告成!

image-20211115174912648

最后再啰嗦一句,如果此时Accept为*/*的话,会以xml形式返回,因为它对应的HttpMessageConvertor先被加载到。尽管在AbstractMessageConverterMethodProcessor->writeWithMessageConverters的最后,如果有匹配多个mediaTypesToUse,会利用MediaType.sortBySpecifityAndQuality进行排序。

image-20211115204301638

针对这个方法,主要是两个comparator,

mediaTypes.sort(MediaType.SPECIFICITY_COMPARATOR.thenComparing(MediaType.QUALITY_VALUE_COMPARATOR));
image-20211115210236647 image-20211115205842645

? 通过debug可以看到,对于application/json和application/xml,他们属于Type一致,并且quality一致,但是子类型不一致的情况,会返回0,即排序认为是相等的,不会交换顺序,也就是以进入list的顺序为准。

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-11-17 13:06:20  更:2021-11-17 13:08:51 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 6:48:35-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码