2024-11-30 09:03:55
本文主要讲述了一个具有"随机性"的反序列化错误!
前言Fastjson作为一款高性能的JSON序列化框架,使用场景众多,不过也存在一些潜在的bug和不足。本文主要讲述了一个具有"随机性"的反序列化错误!
问题代码为了清晰地描述整个报错的来龙去脉,将相关代码贴出来,同时也为了可以本地执行,看一下实际效果。
StewardTipItempackagetest;importjava.util.List;publicclassStewardTipItem{privateIntegertype;privateList<String>contents;publicStewardTipItem(Integertype,List<String>contents){this.type=type;this.contents=contents;}}StewardTipCategory反序列化时失败,此类有两个特殊之处:
返回StewardTipCategory的build方法(忽略返回null值)。
构造函数『C1』Map<Integer,List
>items参数与Listitems属性同名,但类型不同!packagetest;importjava.util.ArrayList;importjava.util.List;importjava.util.Map;publicclassStewardTipCategory{privateStringcategory;privateList<StewardTipItem>items;publicStewardTipCategorybuild(){returnnull;}//C1下文使用C1引用该构造函数publicStewardTipCategory(Stringcategory,Map<Integer,List<String>>items){List<StewardTipItem>categoryItems=newArrayList<>();for(Map.Entry<Integer,List<String>>item:items.entrySet()){StewardTipItemtipItem=newStewardTipItem(item.getKey(),item.getValue());categoryItems.add(tipItem);}this.items=categoryItems;this.category=category;}//C2下文使用C2引用该构造函数publicStewardTipCategory(Stringcategory,List<StewardTipItem>items){this.category=category;this.items=items;}publicStringgetCategory(){returncategory;}publicvoidsetCategory(Stringcategory){this.category=category;}publicList<StewardTipItem>getItems(){returnitems;}publicvoidsetItems(List<StewardTipItem>items){this.items=items;}}StewardTippackagetest;importjava.util.ArrayList;importjava.util.List;importjava.util.Map;publicclassStewardTip{privateList<StewardTipCategory>categories;publicStewardTip(Map<String,Map<Integer,List<String>>>categories){List<StewardTipCategory>tipCategories=newArrayList<>();for(Map.Entry<String,Map<Integer,List<String>>>category:categories.entrySet()){StewardTipCategorytipCategory=newStewardTipCategory(category.getKey(),category.getValue());tipCategories.add(tipCategory);}this.categories=tipCategories;}publicStewardTip(List<StewardTipCategory>categories){this.categories=categories;}publicList<StewardTipCategory>getCategories(){returncategories;}publicvoidsetCategories(List<StewardTipCategory>categories){this.categories=categories;}}JSON字符串{"categories":[{"category":"工艺类","items":[{"contents":["工艺类-提醒项-内容1","工艺类-提醒项-内容2"],"type":1},{"contents":["工艺类-疑问项-内容1"],"type":2}]}]}FastJSONTestpackagetest;importcom.alibaba.fastjson.JSONObject;publicclassFastJSONTest{publicstaticvoidmain(String[]args){Stringtip="{"categories":[{"category":"工艺类","items":[{"contents":["工艺类-提醒项-内容1","工艺类-提醒项-内容2"],"type":1},{"contents":["工艺类-疑问项-内容1"],"type":2}]}]}";try{JSONObject.parseObject(tip,StewardTip.class);}catch(Exceptione){e.printStackTrace();}}}堆栈信息当执行FastJSONTest的main方法时报错:
com.alibaba.fastjson.JSONException:syntaxerror,expect{,actual[atcom.alibaba.fastjson.parser.deserializer.MapDeserializer.parseMap(MapDeserializer.java:228)atcom.alibaba.fastjson.parser.deserializer.MapDeserializer.deserialze(MapDeserializer.java:67)atcom.alibaba.fastjson.parser.deserializer.MapDeserializer.deserialze(MapDeserializer.java:43)atcom.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer.parseField(DefaultFieldDeserializer.java:85)atcom.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:838)atcom.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:288)atcom.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:284)atcom.alibaba.fastjson.parser.deserializer.ArrayListTypeFieldDeserializer.parseArray(ArrayListTypeFieldDeserializer.java:181)atcom.alibaba.fastjson.parser.deserializer.ArrayListTypeFieldDeserializer.parseField(ArrayListTypeFieldDeserializer.java:69)atcom.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:838)atcom.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:288)atcom.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:672)atcom.alibaba.fastjson.JSON.parseObject(JSON.java:396)atcom.alibaba.fastjson.JSON.parseObject(JSON.java:300)atcom.alibaba.fastjson.JSON.parseObject(JSON.java:573)attest.FastJSONTest.main(FastJSONTest.java:17)问题排查排查过程有两个难点:
不能根据报错信息得到异常时JSON字符串的key,position或者其他有价值的提示信息。
报错并不是每次执行都会发生,存在随机性,执行十次可能报错两三次,没有统计失败率。
经过多次执行之后还是找到了一些蛛丝马迹!下面结合源码对整个过程进行简单地叙述,最后也会给出怎么能在报错的时候debug到代码的方法。
JavaBeanInfo:285行clazz是StewardTipCategory.class的情况下,提出以下两个问题:Q1:Constructor[]constructors数组的返回值是什么?Q2:constructors数组元素的顺序是什么?
参考java.lang.Class#getDeclaredConstructors的注释,可得到A1:
A1
publictest.StewardTipCategory(java.lang.String,java.util.Map<java.lang.Integer,java.util.List<java.lang.String>>)『C1』publictest.StewardTipCategory(java.lang.String,java.util.List<test.StewardTipItem>)『C2』
A2
build()方法,C1构造函数,C2构造函数三者在Java源文件的顺序决定了constructors数组元素的顺序!?下表是经过多次实验得到的一组数据,因为是手动触发,并且次数较少,所以不能保证100%的准确性,只是一种大概率事件。
java.lang.Class#getDeclaredConstructors底层实现是nativegetDeclaredConstructors0,JVM的这部分代码没有去阅读,所以目前无法解释产生这种现象的原因。
前中后数组元素顺序build()C1C2随机C1build()C2C2,C1C1C2build()C2,C1build()C2C1随机C2build()C1C1,C2C2C1build()C1,C2C1C2C2,C1C2C1C1,C2正是因为java.lang.Class#getDeclaredConstructors返回数组元素顺序的随机性,才导致反序列化失败的随机性!
[C2,C1]反序列化成功!
[C1,C2]反序列化失败!
[C1,C2]顺序下探寻反序列化失败时代码执行的路径。
JavaBeanInfo:492行com.alibaba.fastjson.util.JavaBeanInfo#build()方法体代码量比较大,忽略执行路径上的无关代码。\
[C1,C2]顺序下代码会执行到492行,并执行两次(StewardTipCategory#category,StewardTipCategory#items各执行一次)。
结束后创建一个com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer。
JavaBeanDeserializer:49行JavaBeanDeserializer两个重要属性:
privatefinalFieldDeserializer[]fieldDeserializers;
protectedfinalFieldDeserializer[]sortedFieldDeserializers;
反序列化test.StewardTipCategory#items时fieldDeserializers的详细信息。
com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializercom.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer#fieldValueDeserilizer(属性值null,运行时会根据fieldType获取具体实现类)com.alibaba.fastjson.util.FieldInfo#fieldType(java.util.Map<java.lang.Integer,java.util.List<java.lang.String>>)
创建完成执行com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze(com.alibaba.fastjson.parser.DefaultJSONParser,java.lang.reflect.Type,java.lang.Object,java.lang.Object,int,int[])
JavaBeanDeserializer:838行com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>,java.lang.reflect.Type)根据字段类型设置com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer#fieldValueDeserilizer的具体实现类。
DefaultFieldDeserializer:34行test.StewardTipCategory#items属性的实际类型是List。
反序列化时根据C1构造函数得到的fieldValueDeserilizer的实现类是com.alibaba.fastjson.parser.deserializer.MapDeserializer。
执行com.alibaba.fastjson.parser.deserializer.MapDeserializer#deserialze(com.alibaba.fastjson.parser.DefaultJSONParser,java.lang.reflect.Type,java.lang.Object)时报错。
MapDeserializer:228行java.lang.Class#getDeclaredConstructors返回[C2,C1]顺序,反序列化时根据C2构造函数得到的fieldValueDeserilizer的实现类是com.alibaba.fastjson.parser.deserializer.ArrayListTypeFieldDeserializer,反序列化成功。
问题解决代码删除C1构造函数,使用其他方式创建StewardTipCategory。
修改C1构造函数参数名称,类型,避免误导Fastjson。
调试packagetest;importcom.alibaba.fastjson.JSONObject;importjava.lang.reflect.Constructor;publicclassFastJSONTest{publicstaticvoidmain(String[]args){Constructor<?>[]declaredConstructors=StewardTipCategory.class.getDeclaredConstructors();//iftruemustfail!if("publictest.StewardTipCategory(java.lang.String,java.util.Map<java.lang.Integer,java.util.List<java.lang.String>>)".equals(declaredConstructors[0].toGenericString())){Stringtip="{"categories":[{"category":"工艺类","items":[{"contents":["工艺类-提醒项-内容1","工艺类-提醒项-内容2"],"type":1},{"contents":["工艺类-疑问项-内容1"],"type":2}]}]}";try{JSONObject.parseObject(tip,StewardTip.class);}catch(Exceptione){e.printStackTrace();}}}}总结开发过程中尽量遵照规范/规约,不要特立独行StewardTipCategory构造函数C1方法签名明显不是一个很好的选择,方法体除了属性赋值,还做了一些额外的类型/数据转换,也应该尽量避免。
专业有深度开发人员对于使用的技术与框架要有深入的研究,尤其是底层原理,不能停留在使用层面。一些不起眼的事情可能导致不可思议的问题:java.lang.Class#getDeclaredConstructors。
Fastjson框架实现时要保持严谨,报错信息尽可能清晰明了,StewardTipCategory反序列化失败的原因在于,fastjson只检验了属性名称,构造函数参数个数而没有进一步校验属性类型。
<<重构:改善既有代码的设计>>提倡代码方法块尽量短小精悍,Fastjson某些模块的方法过于臃肿。
来源:阿里巴巴大淘宝技术