当前位置: 首页 > 后端技术 > Java

怪事年年有,一个JSON解析导致的“薛定谔BUG”调查过程记录

时间:2023-04-01 16:14:09 Java

前言自己开发这么多年,遇到的BUG数不胜数。但是再复杂的bug,只要仔细研究代码,加上debug,总能找到原因。但是最近在公司遇到了这个bug。这个bug乍一看很简单,其实很邪恶。有一段时间,我什至无法理解。在几天的时间内,复发的可能性非常低。几乎不可能捕捉到任何踪迹。所以这篇文章非常真实的记录了这个bug的发现和排查的全过程。起因是之前有同事提出请求,提交测试。当测试同事测试到一半的时候。发现后台报错com.alibaba.fastjson.JSONException:cannotcasttoString,value:{"code":"00","msg":"success","data":{这里是正确的数据}}在com.alibaba.fastjson.util.TypeUtils.castToInt(TypeUtils.java:564)~[fastjson-1.2.29.jar:na]在com.alibaba.fastjson.serializer.IntegerCodec.deserialze(IntegerCodec.java:89)~[fastjson-1.2.29.jar:na]一看就知道这个错误很简单,就是fastjson转换类型的错误。收到的json与要转换的类型不匹配。基本上查一下代码,一眼就能解决。结果同事看了一会,也没发现什么问题。然后远程在测试环境运行debug,设置断点准备调试,测试同事操作一下,又好了。然后解除断点,正常运行。过了一会儿,又出现了同样的错误。继续下断点,还是正常的。这个现象让同事们有些摸不着头脑。我在群里看到这个,觉得很有意思,结果却要看看不看。这他妈到底是什么?.我接过它并查看了代码,从我最初的样子来看它完全没问题。我简化了代码并处理了一些伪代码,如下:log.info("执行结果为:{}",json);T结果=JSON.parseObject(json,c);返回结果;调用时:LuaResultresult=executeLua("xxxx",LuaResult.class,args);而返回对象的泛型T是一个LuaResult对象,其结构也很简单:publicclassLuaResult{protectedStringcode;受保护的字符串消息;受保护的T数据;...}根据打印出来的json结果,返回的数据是正确的结果,完全符合LuaResult对象的结构。虽然调用的时候没有泛型,但是由于之前的代码没有使用到LuaResult中的数据,所以解析之后还是保留了JsonObject的类型。从运营的角度来看,这完全没问题。同样的数据我也用本地的main方法运行,也能正常解析。我什至尝试了多次本地循环解析和多线程解析,都没有问题。那为什么偶尔一到服务器环境就报错呢?我也决定远程调试,我不相信会有薛定谔的bug。我远程设置断点,测试同事开始做业务,一切完全正常。于是我把断点去掉了,没过多久,测试同事给我发来了报错的截图。一切真的和以前的同事一样。..现在我也是一头雾水,这么简单的报错,看不出问题就好了,调试不了??而日志返回的json字符串是完全正常的业务返回数据。太妖孽了排错(1)首先确定返回的数据是完全正确的,在打印log的时候可以看出来。没有低级错误,json不匹配返回类型。问题很明显,是fastjson的解析问题,而且项目使用的fastjson版本比较老,1.2.29版本。但是问题在哪里呢?是一个简单的错误,但是当数据和结构完全正确的时候,偶尔的报错就很奇怪了。而且不相信调试的时候就恢复正常了,正常运行的时候还会出问题。而在接下来的步骤中,这个bug似乎消失了,无论是debug启动还是正常启动,都不会再出现了。看到这里,肯定有人会说这不容易。换个json解析框架,或者升级fastjson。至于换框架,私下和同事商量过改用jackson。首先,系统中有很多地方需要改,其次,即使改了,问题也不会再出现了。但就问题而言,无异于绕过,没有真正正面解决。我对这个错误的根源非常感兴趣。所以我不打算改变它,只是面对它。对于fastjson的升级,其实也有猜测是fastjson的一些bug导致的。但是你得提供证据,否则即使升级后没有复发,也不能证明是因为升级了fastjson而修复的,因为这个bug是极其偶发的。也就是说,你还是要找到问题的根本原因。仅靠长期观察无法证明是否修复。最后一次偶然的机会,终于在debug中抓到了一个断点。这也说明不存在薛定谔的虫子,至少我在观察的时候能捕捉到。断点在如下位置(部分业务数据做了一些改动)//json为:{"code":"00","msg":"success","data":{"xxx":21,"yyy":5}}//c是LuaResult.classTresut=JSON.parseObject(json,c);使用IDEA的Evaluate工具查看执行结果然后继续重复执行。在按了几十次回车后,终于报错了:Thisisveryweird.有没有,一样的数据,一样的代码。执行几十次后出现错误。但是问题一定出在fastjson这边。下一步就是破解问题的根源。为了抓住这个原因,我真的舍不得马上升级。排错(2)之前提到我在调用的时候没有加泛型,所以抱着试一试的想法在这次调用中加了泛型,把参数换成了TypeReferenceLuaResult>结果=executeLua("xxxx",newTypeReference>>(){},args);然后到断点处,用Evaluate工具诊断,点击几百次,没有出现错误。然后确定应该是泛型引起的。通过搜索相关关键词,找到了一篇关于fastjson的泛型解析的文章。大致意思是:fastjson对于没有定义明确泛型的相同对象,默认会使用最后一个泛型。换句话说,fastjson会缓存同一个对象之前的泛型定义。这样,我就可以大致了解这个滔天bug的偶发原因了。为此,我特地在本地写了一段代码来模拟这个bug的产生。也可以复制到本地运行,fastjson的版本应该是1.2.29。publicstaticvoidmain(String...args)throwsException{try{Stringjson="{\"code\":\"00\",\"msg\":\"success\",\"data\":{\"xxx\":21,\"yyy\":5}}";LuaResult结果=JSON.parseObject(json,LuaResult.class);System.out.println(结果);}catch(Exceptione){log.error("错误",e);}try{Stringjson1="{\"msg\":\"success\",\"data\":\"31\",\"code\":\"00\"}";LuaResultresult=JSON.parseObject(json1,newTypeReference>(){});System.out.println(结果);}catch(Exceptione){log.error("发生错误",e);}//上面两个都可以,第三段执行的时候会显示try{Stringjson="{\"code\":\"00\",\"msg\":\"Success\",\"数据\":{\"xxx\":21,\"yyy\":5}}";LuaResultresult=JSON.parseObject(json,LuaResult.class);System.out.println(result);}catch(Exceptione){log.error("Anerroroccurred",e);}}执行后报错:有意思的是,只有三个代码是这个顺序的时候才会报错,把三个代码的顺序改一下,又可以了。必须出现真相大白!由于项目使用的fastjson版本是1.2.29版本,所以我一个版本一个版本升级,想知道哪个版本修复了这个bug,试了1.2.33版本,bug终于没了.去github找1.2.33版本的release信息,发现作者修复了genericparsingerrorwithoutusingparameters的问题。versionofgenericparsing会产生严重的bug,现在已经到了这个程度,继续研究发现fastjson在1.2.33以下的版本,对同一个对象的泛型存在缓存现象。如果之前一直执行带泛型定义的LuaResult没问题,如果前几个带泛型,后面一个不带泛型执行,就会出问题。会按照上次缓存的泛型来解析,因为之前的泛型都是Integer,所以无论这次json是什么数据,都会按照Integer类型进行转换,从而导致“cannotcastto”的错误诠释”。至于为什么一开始会出现类似薛定谔的错觉,现在有了结论,就是因为测试同学的测试顺序。刚好调试的时候,不是按这个顺序执行的,但是正常运行的时候,是按这个顺序执行的。我想这可能是巧合。知道了根本原因,只需要升级到最新版本就可以了。有人会觉得我兜了一圈,直接升级了。何必费心去查原因呢。一方面,我觉得我们在技术上应该更加严谨,知其所以然,更重要的是知其所以然。我对这是怎么发生的很感兴趣(痴迷)。所以即使升级一个版本号可以解决,也要搞清楚来龙去脉。如果大家喜欢类似的关于bughunting过程的文章,欢迎留言告诉我,以后我会多多安排。我是博赛东,一个有趣的深度开发,关注【原人部落】,会分享技术和开源相关内容。我也会分享一些人生观。