⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7215886869199863869 「nyingping」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

大意了

记者:大爷您有什么特长呀? fastjson:我很快。 记者:25乘以23等于多少? fastjson:等于88。记者:?? fastjson:你就说快不快吧!

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

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

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

然后在一个新项目上,脑抽抽,把gson换成了fastjson,还把spring boot默认支持的jackson换成了fastjson。

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

dateformat优先级

本来是一个风和日丽的下午,一个非常简单的改动需求。接口返回的时间只需要年月日日期类型不需要时分秒。

因为我配置全局时间格式化为yyyy-MM-dd HH:mmss,于是我愉快的在javabean的属性上加了个注解。

@JSONField(format="yyyy-MM-dd")

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

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

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

大意了

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

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

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

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

通过这种方式没复现。

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

得,还是回到源码吧。拉下源码,断点,观察JSONSerializer类,主要是writeWithFormat方法。没有发现问题。 因为怀疑是系统导致的,在源码中搜索'linux''unix'关键字,没有发现。断点整个流程重点观察了一下这部份也没有发现问题。

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

JSONSerializer.dateFormatPattern

这部份涉及到了调整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

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

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

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

$ref循环引用问题

public ResultBody test () {
List<Person> list = new ArrayList<>();
Person obj1 = new Person("张三", 48);
list.add(obj1);

Person obj2 = new Person("李四", 23);
list.add(obj2);

Person obj3 = new Person("王麻子", 17);
list.add(obj3);

List<Person> young = list.stream().filter(e -> e.getAge() <= 45).collect(Collectors.toList());
List<Person> children = list.stream().filter(e -> e.getAge()< 18).collect(Collectors.toList());

HashMap map = new HashMap();
map.put("young", young);
map.put("children", children);

return ResultBody.success(map);
}

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

{
"code":"200",
"message":"成功!",
"result":{
"young":[
{
"age":23,
"name":"李四"
},
{
"age":17,
"name":"王麻子"
}
],
"children":[
{"$ref":"$.result.young[1]"}
]
}
}

我现在并不知道什么循环引用检测,这时候它是我的知识盲区。 此时,我观察到的现象是,youngchildren两个list对象中均引用指向了王麻子这个对象。然后,在第2次children引用的时候它在序列化的时候直接指向了第1个young里相应对象引用。

当然遇到这个问题的时候,我在仔细观察排除了非fastjson的问题以后,这次我学聪明了,我直接来到了github的issues区,搜索$ref

循环引用检测

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

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

fastjson2

连 fastjson 原作者都抛弃了 fastjson 了,哎,开源真难!

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

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

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

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

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

但是,我的代码其实并没有循环引用啊,只是两个子对象引用了同一个对象而已。这算什么?误伤吗? 更重要的,一些控制权应该在使用者手里?

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

现在我的前端并没有使用Fastjson,面对"$ref":"$.result.young[1]"这种文本,它能解析吗?它不能呀。 我测试了一下,好像使用fastjson也并不能解析回来:

解析循环引用

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

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

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

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

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

总结

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

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

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

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

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

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

你们看好fastjson2吗

文章目录
  1. 1. dateformat优先级
  2. 2. $ref循环引用问题
  3. 3. 总结