作者:nyingping

记者:大爷您有什么特长呀?

FastJson:我很快。

记者:23423 乘以 4534 等于多少?

FastJson:等于 2343.

记者:??

FastJson:你就说快不快吧!

这个略显马丽苏的标题,各位看官将就着看吧。主要是怕被喷。FastJson 真的很好,我用不用我喜不喜欢的,太不重要了,我只是觉得不适合我而已。

话说以前 Gson 用得好好的,同事极力推荐我使用 FastJson,说很快云云。尽管我们的系统根本感知不出来这点速度差异。

之前也听说 FastJson 爆出来什么重大漏洞,但对我们基本没什么影响,所以这一点倒是没什么偏见。

然后在一个新项目上,脑抽抽,把 Gson 换成了 FastJson,Spring Boot 默认支持的 Jackson 换成了 FastJson。

然后就开始遇到了一些问题。先声明,这真不是尬黑,为了文章效果,故意网上扒些黑料拼凑起来,本文所提到的问题,都来源于本人最近项目的真实经历。

推荐一个开源免费的 Spring Boot 最全教程:

https://github.com/javastacks/spring-boot-best-practice

dateformat 优先级

本来是一个风和日丽的下午,一个非常简单的改动需求。接口返回的时间只需要年月日日期类型不需要时分秒。因为我配置全局时间格式化为yyyy-MM-dd HH:mmss,于是我愉快的在 javabean 的属性上加了个注解。

Fastjson 很快,但不适合我...._github

本地测试一下,没问题,提交到测试环境,搞定,完美。

然后就接到产品的疑问,改动呢?

我登上去看了一下,唉,没改到啊,日期还是带了时分秒。我大意了啊,这么小的改动,又是在测试环境,就没加验证。

那么现在的直接问题是:FastJson 关于时间配置在局部的配置没有生效,使用的还是全局配置。

现象是,开发环境 Windows 上没有问题,测试环境 Linux 上出现了问题。两者有什么区别呢?系统问题?

既然怀疑是两个系统导致的问题,那么就在 idea 里模拟一下 linux 系统。在 VM options 添加 -Dos.name=linux

这不能完全模拟 linux 系统,只针对通过System.getproperty("os.name")来判断当前系统做某些操作的时候有用。

通过这种方式没复现,我又想到了远程调试。

一阵操作猛如虎,远程调试倒是能进断点,只是断点进不了第三方 jar 包的源码。等于白搞。

得,还是回到源码吧。拉下源码,断点,观察 JSONSerializer 类,主要是writeWithFormat方法。没有发现问题。

因为怀疑是系统导致的,在源码中搜索'linux''unix'关键字,没有发现。断点整个流程重点观察了一下这部份也没有发现问题。

突然在 JSONSerializer.dateFormatPattern上发现了这段注释。

Fastjson 很快,但不适合我...._开源项目_02

这部份涉及到了调整 dateformat 的问题,重点在这个#1868,这通常是 github 的问题编号。

1.对于开源项目来说,解决了 BUG,通常会把问题编号放到注释里面去。前提是注释有必要。通过问题编号可以看到问题的前因后果。

2.通常来说,对于 github 开源项目都有 issue 区,拿着这个到编号直接到 issue 一搜就能搜到。

3.但也有一些项级项目,如 spark,flink 是没有 issue 区的,它们的类型问题发现描述追踪都使用 jira 平台。如:

https://issues.apache.org/jira/browse/SPARK-38349

在提交 PR 的时候标题也严格按照[jira 编号][spark 子模块(如core/sql) title]的规则来。

所以拿着这个编号到issue区,不管有没有issue区,也都可以直接到pullrequest区直接搜索,就算 PR 标题里没有问题编号,PR 描述肯定也是有的,只要是有严格 PR 流程的开源项目。

所以这个问题在这里:

https://github.com/alibaba/fastjson/issues/1868

相应的 PR 在这里:

https://github.com/alibaba/fastjson/pull/2706

通过 ISSUES 描述的已知信息,可以看出他遇到的问题跟我是一样的,而这个问题早在 2018 年就提出了。但问题描述不太专业,没有涉及到环境以及最重要的 FastJson 的版本问题。

而通过 PR 可知,这个问题最终在 2020 才解决,期间仅在 ISSUES 区提出的相同问题就有 #1868 #1968 #2029 #24524 个。

解决问题的版本为:1.2.72.

这个信息很关键。我对照了我开发环境的版本,是高于 1.2.72 的,所以没有出现测试环境的问题。

所以,柯南告诉我们,排除了所有可能性,剩下的哪怕再可笑,也是最终问题所在。

那就是,测试环境所用的 FastJson 版本是低于 1.2.72 的。

这种可能性是存在的,因为我们用的是 maven 打代码包,依赖包单独存在。

我最终在测试环境的依赖包目录下发现了两个 Fastjson 包,果然不出所料,有一个 1.2.53 的低版本,它就是罪魁祸首。

所以,最终这个问题有相当大的程度是由于我们团队自身问题引发的。但通过解决这个问题的过程也发现了一些有意思的情况。

首先,FastJson 在某一个版本为什么会引发这个问题。它肯定是某个 PR 改出问题的,rv,testcase 覆盖没有到位。

其次,从试图解决这个问题的 3 个 PR 的时间线,分别在 2018 年,2019 年,2020 年。说明,FastJson 这个项目的 contributor 看起来有百来人,但其中过于依赖其中某 1 个或者某些主力人员。精力有限,某些优先级不那么高的 BUG 只能放任。

Fastjson 很快,但不适合我...._开源项目_03

同时这个项目的荣誉感并没有那么高(或者叫并没有那么吸引高手),它并不是 Apache 顶级项目,要是其它诸如 Spark、Flink、Spring,哪怕是 Dubbo 呢,很想象这些项目会有一个并不算复杂的 BUG 悬而未决长达 3 年时间。在这些顶级开源项目,大家都是拼了老命的想找些 BUG 来提交 PR。

当然,以上只是我个人的一点猜测。

复盘,遇到 FastJson 的问题,一开始就应该奔着 github 的 issues 区,它大概率已经被前人踩坑了。

$ref 循环引用问题

Fastjson 很快,但不适合我...._github_04

以上测试接口返回前端什么?

Fastjson 很快,但不适合我...._开源项目_05

我现在并不知道什么循环引用检测,这时候它是我的知识盲区。此时,我观察到的现象是,youngchildren两个 list 对象中均引用指向了王麻子这个对象。然后,在第 2 次children引用的时候它在序列化的时候直接指向了第 1 个young里相应对象引用。当然遇到这个问题的时候,我在仔细观察排除了非 fastjson 的问题以后,这次我学聪明了,我直接来到了 Github 的 issues 区,搜索$ref

Fastjson 很快,但不适合我...._json_06

果然有很多同道中人,近 150 个问题,从时间上来看还挺新鲜。我点击了 closed,既然关闭了,那肯定解决了吧。

我点进了 closed 区第一个问题,然后作者让升级到 fastjson2。???

Fastjson 很快,但不适合我...._开源项目_07

如果我没有理解错,FastJson 和 FastJson2 可不是两个版本的区别,是两个项目也!据说 API 也有兼容性问题。直接这样升级过去,谈何容易!

我觉得这也是个槽点,FastJson 好像并没有一个稳定维护的版本,遇到问题总是在升级,升级的过程中也没做好质量控制,又引入了新的问题。

还是在当前项目寻求解决方法吧,哪怕升版本也好啊。终于在另一个问题下面找到了问题所在以及解决方案。

https://github.com/alibaba/fastjson/issues/3643

我现在知道这是由于循环引用检测引起的。通过设置SerializerFeature.DisableCircularReferenceDetect可以避免这个问题。

但是,我的代码其实并没有循环引用啊,只是两个子对象引用了同一个对象而已。这算什么?误伤吗?

更重要的,一些控制权应该在使用者手里?

比如,当前这个循环引用在序列化会出的问题,应该是用户手动去开启,而不是默认给用户开启。在优先级上,全局应该关闭,在有循环引用的地方,让用户选择局部开启。

现在我的前端并没有使用 FastJson,面对"$ref":"$.result.young[1]"这种文本,它能解析吗?它不能呀。

我测试了一下,好像使用 FastJson 也并不能解析回来:

Fastjson 很快,但不适合我...._json_08

:经提醒,这里应使用完整报文解析,经测试,确实可以。感谢提醒!

更可怕的问题是,刚好在测试环节有两个子对象引用了同一个对象,被我提前发现了。如果测试环境没有这样的情况,在生产环境刚好遇到了呢?那就是生产事故了呀。

本来是一个挺好的设计点,能起到锦上添花的作用,但它却可能暴雷,这是好心办坏事。

同样的,还有SerializerFeature.WriteMapNullValue。如果一个字段值为null,fastjson 默认就不返回该字段了。本来前后端约定好,如果为null就怎样处理的逻辑,可能在生产环境中突然暴雷啊。

就像WriteNullListAsEmpty就很好,不错的设计点,如果返回的 list 为null的时候,用户可以选择让它序列化为[],但它也不是默认开启的呀,给了用户额外的选择权,对吧。

总结

写到这里的时候,我是真心觉得 FastJson 有比竞品有些特色的地方。这真不是为了所谓的客观公正,非要负面写多点,再搞点正面的。

为了写文章,那肯定要去试验,得把竞品也拿出来测试一下,一测试发现并不是 FastJson 独有的,尴尬!

但我还是那句话,不管你信不信,对于开源项目,特别是这样一个广泛使用的开源项目,肯定有非常值得学习的地方。一个开源项目,如果整天拿着显微镜去观察,那肯定能找出不少毛病。

这里稍微总结一下本文的信息点。并不一定是某个具体 BUG,而是通过这个 BUG,解决这个 BUG 背后所展现出来的 FastJson 的信息或趋势。

  1. reviewtestcase 覆盖不是很到位
  2. contributor 看起来很多,但严重依赖主力人员。而主力精力有限,某些优先级不那么高的 BUG 只能放任。
  3. 这个项目的荣誉感并没有那么高,或者叫并没有那么吸引高手)。
  4. 有些功能点应该把控制主动权交给用户,如 DisableCircularReferenceDetectWriteMapNullValue 等。默认开启非常容易导致线上暴雷。
  5. 作者已经全面转向 fastjosn2,而且哪怕在这之前,对于 fastjson 没有一个稳定维护的版本,不断升级,不断引入新问题。

祝愿 fastjson2 越来越好,不要步 struts2 的后尘。