为了清晰地描述整个报错的来龙去脉,将相关代码贴出来,同时也为了可以本地执行,看一下实际效果。
▐ StewardTipItem
package test; import java.util.List; public class StewardTipItem { private Integer type; private List<String> contents; public StewardTipItem(Integer type, List<String> contents) { this.type = type; this.contents = contents; } }
▐ StewardTipCategory
反序列化时失败,此类有两个特殊之处:
- 返回StewardTipCategory的build方法(忽略返回null值)。
- 构造函数『C1』Map<Integer, List<String>> items参数与List<StewardTipItem> items属性同名,但类型不同!
package test; import java.util.ArrayList; import java.util.List; import java.util.Map; public class StewardTipCategory { private String category; private List<StewardTipItem> items; public StewardTipCategory build() { return null; } //C1 下文使用C1引用该构造函数 public StewardTipCategory(String category, Map<Integer, List<String>> items) { List<StewardTipItem> categoryItems = new ArrayList<>(); for (Map.Entry<Integer, List<String>> item : items.entrySet()) { StewardTipItem tipItem = new StewardTipItem(item.getKey(), item.getValue()); categoryItems.add(tipItem); } this.items = categoryItems; this.category = category; } // C2 下文使用C2引用该构造函数 public StewardTipCategory(String category, List<StewardTipItem> items) { this.category = category; this.items = items; } public String getCategory() { return category; } public void setCategory(String category) { this.category = category; } public List<StewardTipItem> getItems() { return items; } public void setItems(List<StewardTipItem> items) { this.items = items; } }
▐ StewardTip
package test; import java.util.ArrayList; import java.util.List; import java.util.Map; public class StewardTip { private List<StewardTipCategory> categories; public StewardTip(Map<String, Map<Integer, List<String>>> categories) { List<StewardTipCategory> tipCategories = new ArrayList<>(); for (Map.Entry<String, Map<Integer, List<String>>> category : categories.entrySet()) { StewardTipCategory tipCategory = new StewardTipCategory(category.getKey(), category.getValue()); tipCategories.add(tipCategory); } this.categories = tipCategories; } public StewardTip(List<StewardTipCategory> categories) { this.categories = categories; } public List<StewardTipCategory> getCategories() { return categories; } public void setCategories(List<StewardTipCategory> categories) { this.categories = categories; } }
▐ JSON字符串
{ "categories":[ { "category":"工艺类", "items":[ { "contents":[ "工艺类-提醒项-内容1", "工艺类-提醒项-内容2" ], "type":1 }, { "contents":[ "工艺类-疑问项-内容1" ], "type":2 } ] } ] }
▐ FastJSONTest
package test; import com.alibaba.fastjson.JSONObject; public class FastJSONTest { public static void main(String[] args) { String tip = "{\"categories\":[{\"category\":\"工艺类\",\"items\":[{\"contents\":[\"工艺类-提醒项-内容1\",\"工艺类-提醒项-内容2\"],\"type\":1},{\"contents\":[\"工艺类-疑问项-内容1\"],\"type\":2}]}]}"; try { JSONObject.parseObject(tip, StewardTip.class); } catch (Exception e) { e.printStackTrace(); } } }
▐ 堆栈信息
当执行FastJSONTest的main方法时报错:
com.alibaba.fastjson.JSONException: syntax error, expect {, actual [ at com.alibaba.fastjson.parser.deserializer.MapDeserializer.parseMap(MapDeserializer.java:228) at com.alibaba.fastjson.parser.deserializer.MapDeserializer.deserialze(MapDeserializer.java:67) at com.alibaba.fastjson.parser.deserializer.MapDeserializer.deserialze(MapDeserializer.java:43) at com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer.parseField(DefaultFieldDeserializer.java:85) at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:838) at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:288) at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:284) at com.alibaba.fastjson.parser.deserializer.ArrayListTypeFieldDeserializer.parseArray(ArrayListTypeFieldDeserializer.java:181) at com.alibaba.fastjson.parser.deserializer.ArrayListTypeFieldDeserializer.parseField(ArrayListTypeFieldDeserializer.java:69) at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:838) at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:288) at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:672) at com.alibaba.fastjson.JSON.parseObject(JSON.java:396) at com.alibaba.fastjson.JSON.parseObject(JSON.java:300) at com.alibaba.fastjson.JSON.parseObject(JSON.java:573)
问题排查
排查过程有两个难点:
- 不能根据报错信息得到异常时JSON字符串的key,position或者其他有价值的提示信息。
- 报错并不是每次执行都会发生,存在随机性,执行十次可能报错两三次,没有统计失败率。
经过多次执行之后还是找到了一些蛛丝马迹!下面结合源码对整个过程进行简单地叙述,最后也会给出怎么能在报错的时候debug到代码的方法。
▐ JavaBeanInfo:285行
clazz是StewardTipCategory.class的情况下,提出以下两个问题:Q1:Constructor[] constructors数组的返回值是什么?Q2:constructors数组元素的顺序是什么?
参考java.lang.Class#getDeclaredConstructors的注释,可得到A1:
- A1
public test.StewardTipCategory(java.lang.String,java.util.Map<java.lang.Integer, java.util.List<java.lang.String>>)『C1』public test.StewardTipCategory(java.lang.String,java.util.List<test.StewardTipItem>)『C2』
- A2
build()方法,C1构造函数,C2构造函数三者在Java源文件的顺序决定了constructors数组元素的顺序!下表是经过多次实验得到的一组数据,因为是手动触发,并且次数较少,所以不能保证100%的准确性,只是一种大概率事件。
java.lang.Class#getDeclaredConstructors底层实现是native getDeclaredConstructors0,JVM的这部分代码没有去阅读,所以目前无法解释产生这种现象的原因。
前 |
中 |
后 |
数组元素顺序 |
build() |
C1 |
C2 |
随机 |
C1 |
build() |
C2 |
C2,C1 |
C1 |
C2 |
build() |
C2,C1 |
build() |
C2 |
C1 |
随机 |
C2 |
build() |
C1 |
C1,C2 |
C2 |
C1 |
build() |
C1,C2 |
C1 |
C2 |
C2,C1 |
|
C2 |
C1 |
C1,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两个重要属性:
- private final FieldDeserializer[] fieldDeserializers;
- protected final FieldDeserializer[] 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行
▐ DefaultFieldDeserializer:53行
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<StewardTipItem>。
反序列化时根据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行
▐ JavaBeanDeserializer:838行
java.lang.Class#getDeclaredConstructors返回[C2,C1]顺序,反序列化时根据C2构造函数得到的fieldValueDeserilizer的实现类是com.alibaba.fastjson.parser.deserializer.ArrayListTypeFieldDeserializer,反序列化成功。
问题解决
▐ 代码
- 删除C1构造函数,使用其他方式创建StewardTipCategory。
- 修改C1构造函数参数名称,类型,避免误导Fastjson。
▐ 调试
package test; import com.alibaba.fastjson.JSONObject; import java.lang.reflect.Constructor; public class FastJSONTest { public static void main(String[] args) { Constructor<?>[] declaredConstructors = StewardTipCategory.class.getDeclaredConstructors(); // if true must fail! if ("public test.StewardTipCategory(java.lang.String,java.util.Map<java.lang.Integer, java.util.List<java.lang.String>>)".equals(declaredConstructors[0].toGenericString())) { String tip = "{\"categories\":[{\"category\":\"工艺类\",\"items\":[{\"contents\":[\"工艺类-提醒项-内容1\",\"工艺类-提醒项-内容2\"],\"type\":1},{\"contents\":[\"工艺类-疑问项-内容1\"],\"type\":2}]}]}"; try { JSONObject.parseObject(tip, StewardTip.class); } catch (Exception e) { e.printStackTrace(); } } } }
总结
▐ 开发过程中尽量遵照规范/规约,不要特立独行
StewardTipCategory构造函数C1方法签名明显不是一个很好的选择,方法体除了属性赋值,还做了一些额外的类型/数据转换,也应该尽量避免。
▐ 专业有深度
开发人员对于使用的技术与框架要有深入的研究,尤其是底层原理,不能停留在使用层面。一些不起眼的事情可能导致不可思议的问题:java.lang.Class#getDeclaredConstructors。
▐ Fastjson
框架实现时要保持严谨,报错信息尽可能清晰明了,StewardTipCategory反序列化失败的原因在于,fastjson只检验了属性名称,构造函数参数个数而没有进一步校验属性类型。
<<重构:改善既有代码的设计>>提倡代码方法块尽量短小精悍,Fastjson某些模块的方法过于臃肿。