翻阅到相似的例子
接下来就还是一直在网上翻阅其他资料,后来在掘金找到一篇: https://juejin.cn/post/6929740384734019597
文章提到的问题现象跟我们的还挺像的,都是一开始正常,过了一会儿就又出现问题。
文章对FastJson剖析挺深,感觉我们的工程不至于这么奇葩吧。还提到了什么getKoltinConstructorParameters、Unit变量、kotlin_error标记位问题。核心原因是有个静态标记位被置为异常状态。
虽然抽象,但死马当活马医,试试看吧。
先看工程里有没有打印这个NPE吧,SLS日志搜了半天都没搜到,去源码看了一下,原来这里只打印了异常堆栈,那不会被采集到SLS中,不过服务器日志里可能会有。
妈耶还真给我搜到了,再仔细读了一下打堆栈的地方,会将kotlin_error置为true,而且关键的地方在于这玩意儿是个static volatile变量!!具有静态共享特征(static)和多线程可见性(volatile),意味着整个工程使用的都是被修改后的值。
- ☆ 锁定关键报错位置
继续进QuestionCardProxyApi 228行 找到报错源,发现确实有个toJsonString。对于invokeResumeReq这个对象,我一眼看到有个古怪的赋值,this.resumeBody被赋值为{}。但这个赋值有何神奇之处,哪能这么个小玩意儿导致全局异常啊??
这段日志代码是12-17添加的,时间看了下还挺符合。再看resumeBody其实是个Java类的一个object类型字段,kotlin中这样对其赋值,编译器倒也没报错。
其实到这个地方已经感觉要水落石出了,再一看master,妈耶这个代码上线了,但线上却没有任何报错,运行一切正常。
但基本能确定是这里无疑了,就赶紧拉写这段代码的同学看(悄悄说,仁兄一开始提给我的bug),确认线上还没开始灰度,无任何流量后放心了。他在家里加班修,我继续看原理。
首先肯定本意是想将resumeBody置空,误用了{},{}在kotlin中被编译器解释为一个lambda表达式。
再看debug信息,这里resumeBody实际上被解释为 ()->kotlin.Unit 这样一个lambda函数,arity表示函数的参数数量为0.
也就是说:{}是一个没有任何入参,并且返回值默认为Unit类型的一个函数(没有明确返回值时默认Unit类型)。
为了更清晰地了解这个语法,可以看下面这个例子:这里定义了一个函数式变量x,入参为s,出参为s+“000”。在使用时可以直接以函数的方式调用x,最终得到y=111000
FastJson自然无法正确解析这样的一个Object字段,而最令人细思极恐的问题是 解析这个对象报了错,却把kotlin_error置为true,而后有没有地方将其复原为false,导致所有的反序列化都进到这里,返回不正确的结果,错误结果被外层拦截抛出default constructor not found异常。这个影响面是巨大的,会导致整个工程崩溃。
具体代码解释:
1.一开始 kotlin_error !=true ,会进到 kotlin_kclass_getConstructors.invoke获取类构造器,这里抛错了,把kotlin_error改为true;
2.往后所有相关的FastJson序列化、反序列化重新进到这里都会return null,导致类构造器获取失败;
3.外层没拿到paramNames,直接抛了异常。
- 有问题的代码(自测)
有kotlin运行环境的同学可以尝试运行下面的代码自测。
注意:经测试这段代码在FastJson 2.0.53版本可以正常运行,其他版本同学们可以再自测下。
四、总结&反思
至此,问题定位清楚并彻底解决了。这次bug是工作以来碰到的最抽象的一个,耗时两天。虽然有点低效,但找到原因后俺非常激动,逮住组里同学细细讲了一番,估计这么抽象的问题工作多年的大佬也不一定能遇到。
再次膜拜一下掘金大佬:https://juejin.cn/post/6929740384734019597
另外也反思了以下几个问题:
1.工程中Java、kotlin、groovy等多语言混编,对开发同学的语法掌握程度有较高要求,有时候各语言间会混淆,特别是判空、变量定义规则等。这些语言的线上空指针我都尝过,有点酸爽。这次同学出现语法问题,其实也是有点语言混淆了,如果纯Java,铁定不会甩个{}上去;
2.这次线上无异常,得益于灰度开关(哥们可得感谢我,还没放量,否则一旦流量进来,涉及kotlin的链路全部中断就寄了);
3.FastJson是有很多漏洞的,使用时仍然要高度注意。任何框架都是不能完全信任的,毕竟代码都是人写出来的,Bug要大家一起合力发现hhh;