Fastjson 反序列化隨機性失敗

本文主要講述了一個具有 "隨機性" 的反序列化錯誤!

前言

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. 返回 StewardTipCategory 的 build 方法 (忽略返回 null 值)。

  2. 構造函數『C1』Map<Integer, List> items 參數與 List 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:

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』

build() 方法,C1 構造函數,C2 構造函數三者在 Java 源文件的順序決定了 constructors 數組元素的順序!

下表是經過多次實驗得到的一組數據,因爲是手動觸發,並且次數較少,所以不能保證 100% 的準確性,只是一種大概率事件。

java.lang.Class#getDeclaredConstructors 底層實現是 native getDeclaredConstructors0,JVM 的這部分代碼沒有去閱讀,所以目前無法解釋產生這種現象的原因。

jDVKlb

正是因爲 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#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>>)

創建完成執行 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

反序列化時根據 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 某些模塊的方法過於臃腫。

作者 | 崔亞斌

編輯 | 橙子君

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/CFw6nNc3Vo2v3vPyejo5TQ