Fastjson反序列化随机性失败

淘系技术 2022-06-24 16:20

本文主要讲述了一个具有"随机性"的反序列化错误!



前言


Fastjson作为一款高性能的JSON序列化框架,使用场景众多,不过也存在一些潜在的bug和不足。本文主要讲述了一个具有"随机性"的反序列化错误!


问题代码

为了清晰地描述整个报错的来龙去脉,将相关代码贴出来,同时也为了可以本地执行,看一下实际效果。

  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


反序列化时失败,此类有两个特殊之处:
  1. 返回StewardTipCategorybuild方法(忽略返回null值)。
  2. 构造函数C1Map<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)  at test.FastJSONTest.main(FastJSONTest.java:17)


问题排查

排查过程有两个难点:

  1. 不能根据报错信息得到异常时JSON字符串的key,position或者其他有价值的提示信息。
  2. 报错并不是每次执行都会发生,存在随机性,执行十次可能报错两三次,没有统计失败率。

经过多次执行之后还是找到了一些蛛丝马迹!下面结合源码对整个过程进行简单地叙述,最后也会给出怎么能在报错的时候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返回数组元素顺序的随机性,才导致反序列化失败的随机性!
  1. [C2,C1]反序列化成功!
  2. [C1,C2]反序列化失败!

[C1,C2]顺序下探寻反序列化失败时代码执行的路径。


  JavaBeanInfo:492行



com.alibaba.fastjson.util.JavaBeanInfo#build()方法体代码量比较大,忽略执行路径上的无关代码。

  1. [C1,C2]顺序下代码会执行到492行,并执行两次(StewardTipCategory#category, StewardTipCategory#items各执行一次)。
  2. 结束后创建一个com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer


  JavaBeanDeserializer:49行



JavaBeanDeserializer两个重要属性:
  1. private final FieldDeserializer[]   fieldDeserializers;
  2. protected final FieldDeserializer[] sortedFieldDeserializers;

反序列化test.StewardTipCategory#itemsfieldDeserializers的详细信息。

com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer
com.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,反序列化成功。


问题解决

  代码


  1. 删除C1构造函数,使用其他方式创建StewardTipCategory。
  2. 修改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某些模块的方法过于臃肿。


吾生也有涯,而知也无涯


团队介绍

每平每屋·设计家,作为阿里巴巴旗下家装家居设计平台,为家装家居企业和设计师提供专业设计工具和渲染服务,同时依托阿里商业生态,帮助设计师和企业打通设计与商品全链路,推动家装家居设计全流程数字化。

基于云计算和AI为核心,以3D云设计工具为技术底层,提供产业上下游数字化基础设施;作为产业数字化解决方案提供商,提供全生命周期数字化产品,助力产业商家完成数字化升级转型;新渠道设计带单驱动以设计师为中心的全域营销数字化,增加商家销售渠道;推出云管家模式,为商家提供从精准获客到用户服务的装修全流程产业服务一体化解决方案,加速全流程服务一体化。

如果您对我们做的事情感兴趣可将简历发送至topping-zhaopin@alibaba-inc.com,期待您的加入!


✿  拓展阅读
 

作者|崔亚斌
编辑|橙子君
推荐阅读