Fastjson反序列化随机性失败

2年前 (2022) 程序员胖胖胖虎阿
230 0 0

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

前言

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

反序列化时失败,此类有两个特殊之处:

  • 返回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)
  at test.FastJSONTest.main(FastJSONTest.java:17)

问题排查

排查过程有两个难点:

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

经过多次执行之后还是找到了一些蛛丝马迹!下面结合源码对整个过程进行简单地叙述,最后也会给出怎么能在报错的时候debug到代码的方法。

JavaBeanInfo:285行

Fastjson反序列化随机性失败
clazz是StewardTipCategory.class的情况下,提出以下两个问题:Q1:Constructor[] constructors数组的返回值是什么?Q2:constructors数组元素的顺序是什么?参考java.lang.Class#getDeclaredConstructors的注释,可得到A1:
Fastjson反序列化随机性失败

  • 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行

    Fastjson反序列化随机性失败
    com.alibaba.fastjson.util.JavaBeanInfo#build()方法体代码量比较大,忽略执行路径上的无关代码。

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

JavaBeanDeserializer:49行

Fastjson反序列化随机性失败
JavaBeanDeserializer两个重要属性:

  • private final FieldDeserializer[]   fieldDeserializers;
  • protected final FieldDeserializer[] sortedFieldDeserializers;

反序列化test.StewardTipCategory#items时fieldDeserializers的详细信息。

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>>)
Fastjson反序列化随机性失败
创建完成执行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行

Fastjson反序列化随机性失败

DefaultFieldDeserializer:53行

Fastjson反序列化随机性失败
com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>, java.lang.reflect.Type)根据字段类型设置com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer#fieldValueDeserilizer的具体实现类。

DefaultFieldDeserializer:34行

Fastjson反序列化随机性失败

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行

Fastjson反序列化随机性失败

JavaBeanDeserializer:838行

Fastjson反序列化随机性失败
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某些模块的方法过于臃肿。

吾生也有涯,而知也无涯

版权声明:程序员胖胖胖虎阿 发表于 2022年10月3日 上午4:24。
转载请注明:Fastjson反序列化随机性失败 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...