SpringBoot 应用出错 Comparison method violates its general contract!

出现此错误的大致环境如下

  1. SpringBoot 2.7.17, SpringWeb 项目,所引用入的 spring-webmvc-5.3.30, spring 6 已解决
  2. JDK 1.8 或 JDK 17
  3. 依赖了 jackson-dataformat-xml:2.12.6 和 jackson-dataformat-cbor:2.12.6, 它会在 RestTemplate 加上 application/xml, application/cbor 等 Accept 类型
  4. 代码中用 RestTemplate 调用此应用的 Endpoint, 未设置任何头

后面会详细列出能重现此问题的 pom.xml 配置及 Java 代码

在执行

restTemplate.getForEntity("http://localhost:8080/test2", String.class)

时出现如下错误

问题出在对 MediaType  的排序上

前面先摆出了错误,现在详细列出项目代码

Maven pom.xml

可能不会直接依赖 jackson-dataformat-cbor, 但 aws-java-sdk-lambda 会依赖于它。

DemoController.java

代码很简单,/test1 中用 RestTemplate 调用 /test2, 在执行

return restTemplate.getForEntity("http://localhost:8080/test2", String.class).getBody();

时出现前面的错误

我们可以用 curl 命令来测试

curl http://localhost:8080/test1

我们跟踪到错误代码行 MediaType.sortBySpecificityAndQuality(List<MediaType> mediaType)

不就是对一个列表进行排序,为何会出错。十分好奇这其中会有什么内容

50 个元素,从对像的 ID 来看,里面有许多重复的元素,从这点来看就是 Spring 的问题。如果去除重复的元素就只有 6160 ~ 6171,共 19 个元素。

如果调试光标停在此处直接执行 

mediaTypes.sort(MediaType.SPECIFICITY_COMPARATOR.thenComparing(MediaType.QUALITY_VALUE_COMPARATOR))

就会出现最前面的错误

再次执行错误则消失,其实只要执行 mediaTypes.sort(MediaType.SPECIFICITY_COMPARATOR) 这部分代码就会出错

那这 50 个大量重复的元素是如何得来的呢,进到 org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor

从两个已经有重复的元素中再次组合出来的。

Google 一下大概知道问题出在排序上,TimSort 要求元素大小有传递性,如已知 A>B, B>C, 那么 A 必须大于 C。本人通过简单的自定义 Comparator 的方式尚未能用简单的代码重现出相同的错误。不过我们可以从上面提取出待排序的列表,找出相关的 Comparator 实现,剔除不必要的逻辑,得到下面可重现相同问题的代码

MediaType 是 Spring Web 的 org.springframework.http.MediaType

有了上面简短的可重现错误的代码,我们可以有两个方向上的解决办法

  1. 如何规避严格的 TimSort 排序规则
  2. 如何改变 List<MediaType> 列表

使用早先的归并排序

TimSort 是 JDK 1.8 加进来的,想要避免用 TimSort,即用 JDK 1.8 之前的排序方式,可以配置系统属性,如用 Java 代码

System.setProperty("java.util.Arrays.useLegacyMergeSort", "true");

有了这一系统属性后,执行前面的 ArraySortErrorDemo 就不会出错了。JDK 应用该属性选择排序算法的代码在 java.util.Arrays 类中,其中相关代码

算法选择

用传统的 legacyMergeSort() 就不会有问题,TimSort 产生了麻烦。

使用 RestTemplate 时加上 Accept 头

通过明确设定 RestTemplate 的 Accept 头会影响到 Spring 获得的所支持 MediaType 的列表,在一定程度上减少 TimSort 排序时出问题的概率,但不能完全避免。

RestTemplate  请求  /test2 的代码改成

根据实际 /test2 返回的类型,Accept 头改成 text/plain 等都可正常得到响应。当 Accept: */* 时从 /test1 中的 RestTemplate 发出的 HTTP 请求是

GET /test2 HTTP/1.1\r\n
Accept: */*\r\n
User-Agent: Java/17.0.9\r\n
Host: localhost:8080\r\n
Connection: keep-alive\r\n
\r\n

但是用 RestTemplateBuild 设置 DefaultHeader 就不行了,比如下面的用法仍然用相同的出错出现

在 defaultHeader 中设置的 Accept 对 RestTemplate 没有任何影响,比如我们把上面的 defaultHeader(HttpHeaders.ACCEPT, "*/*) 改成

.defaultHeader(HttpHeaders.ACCEPT, "

观察到的 HTTP 请求是

GET /test2 HTTP/1.1\r\n
Accept: text/plain, application/xml, text/xml, application/json, application/cbor, application/*+xml, application/*+json, */*\r\n
User-Agent: Java/17.0.9\r\n
Host: localhost:8080\r\n
Connection: keep-alive\r\n
\r\n

Accept 头没有变化

升级到 Spring Boot 3

如果把 Spring Boot 升级到 3, 所引入的 Spring 是 6,它已解决此问题。SpringBoot 2 中只提升 spring-web 到 6.0 会有兼容问题,如找不到 jakarta/servlet/ServletRequest 等。

Spring 6 在 org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor 中对 MediaType 的排序算法用了冒泡排序,在该类的方法

Spring Web 在获得了 MediaType 列表后,不再直接调用 Arrays.sort() 方法,JDK 1.8 以上会使用 TimSort 排序算法,从而避开了 Comparison method violates its general contract! 的错误。 

冒泡排序无疑是最低效的,不过这个列表中的元素不会太多,不会有太大的关系。

总结

  1. 从 Spring 6 对此问题的解决,他们也意识到 JDK 1.8 的 TimSort 是个问题,过份要求元素的排序规则
  2. 其实 Spring 在取得 MediaType 列表之后应该根据对象引用去重后出现的元素
  3. RestTemplate 自动会带入的 Accept 头的值请参考 RestTemplate 的默认构造函数,其中会根据是否可找到类附加 Accept 头的值
  4. 如果清楚不会用到 application/cbor,可从 Maven 中把 jackson-dataformat-cbor 依赖排除
  5. 一方面是更新到 JDK 8 用到了 TimSort 的问题,更应该及时更新到 SpringBoot 3 让新的  Spring 6 帮我们解决该问题
  6. 使用 "java.util.Arrays.useLegacyMergeSort" 会影响到系统全局,可能会降低某些列表排序的性能,给 RestTemplate 添加 Accept 头不是稳妥的解决办法,升级依赖才是王道

 

本文链接 https://yanbin.blog/springboot-error-comparison-method-violates-its-general-contract/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments