Fastjson反序列化隨機(jī)性失敗示例詳解
前言
本文主要講述了一個(gè)具有"隨機(jī)性"的反序列化錯(cuò)誤!
Fastjson作為一款高性能的JSON序列化框架,使用場(chǎng)景眾多,不過(guò)也存在一些潛在的bug和不足。本文主要講述了一個(gè)具有"隨機(jī)性"的反序列化錯(cuò)誤!
問(wèn)題代碼
為了清晰地描述整個(gè)報(bào)錯(cuò)的來(lái)龍去脈,將相關(guān)代碼貼出來(lái),同時(shí)也為了可以本地執(zhí)行,看一下實(shí)際效果。
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
反序列化時(shí)失敗,此類有兩個(gè)特殊之處:
- 返回StewardTipCategory的build方法(忽略返回null值)。
- 構(gòu)造函數(shù)『C1』Map<Integer, List> items參數(shù)與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引用該構(gòu)造函數(shù)
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引用該構(gòu)造函數(shù)
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":[
"工藝類-提醒項(xiàng)-內(nèi)容1",
"工藝類-提醒項(xiàng)-內(nèi)容2"
],
"type":1
},
{
"contents":[
"工藝類-疑問(wèn)項(xiàng)-內(nèi)容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":["工藝類-提醒項(xiàng)-內(nèi)容1","工藝類-提醒項(xiàng)-內(nèi)容2"],"type":1},{"contents":["工藝類-疑問(wèn)項(xiàng)-內(nèi)容1"],"type":2}]}]}";
try {
JSONObject.parseObject(tip, StewardTip.class);
} catch (Exception e) {
e.printStackTrace();
}
}
}
堆棧信息
當(dāng)執(zhí)行FastJSONTest的main方法時(shí)報(bào)錯(cuò):
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)
問(wèn)題排查
排查過(guò)程有兩個(gè)難點(diǎn):
- 不能根據(jù)報(bào)錯(cuò)信息得到異常時(shí)JSON字符串的key,position或者其他有價(jià)值的提示信息。
- 報(bào)錯(cuò)并不是每次執(zhí)行都會(huì)發(fā)生,存在隨機(jī)性,執(zhí)行十次可能報(bào)錯(cuò)兩三次,沒(méi)有統(tǒng)計(jì)失敗率。
經(jīng)過(guò)多次執(zhí)行之后還是找到了一些蛛絲馬跡!下面結(jié)合源碼對(duì)整個(gè)過(guò)程進(jìn)行簡(jiǎn)單地?cái)⑹?,最后也?huì)給出怎么能在報(bào)錯(cuò)的時(shí)候debug到代碼的方法。
JavaBeanInfo:285行

clazz是StewardTipCategory.class的情況下,提出以下兩個(gè)問(wèn)題:
Q1:Constructor[] constructors數(shù)組的返回值是什么?
Q2:constructors數(shù)組元素的順序是什么?
參考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構(gòu)造函數(shù),C2構(gòu)造函數(shù)三者在Java源文件的順序決定了constructors數(shù)組元素的順序!
?下表是經(jīng)過(guò)多次實(shí)驗(yàn)得到的一組數(shù)據(jù),因?yàn)槭鞘謩?dòng)觸發(fā),并且次數(shù)較少,所以不能保證100%的準(zhǔn)確性,只是一種大概率事件。
java.lang.Class#getDeclaredConstructors底層實(shí)現(xiàn)是native getDeclaredConstructors0,JVM的這部分代碼沒(méi)有去閱讀,所以目前無(wú)法解釋產(chǎn)生這種現(xiàn)象的原因。
| 前 | 中 | 后 | 數(shù)組元素順序 |
|---|---|---|---|
| build() | C1 | C2 | 隨機(jī) |
| C1 | build() | C2 | C2,C1 |
| C1 | C2 | build() | C2,C1 |
| build() | C2 | C1 | 隨機(jī) |
| C2 | build() | C1 | C1,C2 |
| C2 | C1 | build() | C1,C2 |
| C1 | C2 | C2,C1 | |
| C2 | C1 | C1,C2 |
正是因?yàn)閖ava.lang.Class#getDeclaredConstructors返回?cái)?shù)組元素順序的隨機(jī)性,才導(dǎo)致反序列化失敗的隨機(jī)性!
- [C2,C1]反序列化成功!
- [C1,C2]反序列化失??!
[C1,C2]順序下探尋反序列化失敗時(shí)代碼執(zhí)行的路徑。
JavaBeanInfo:492行

com.alibaba.fastjson.util.JavaBeanInfo#build()方法體代碼量比較大,忽略執(zhí)行路徑上的無(wú)關(guān)代碼。\
- [C1,C2]順序下代碼會(huì)執(zhí)行到492行,并執(zhí)行兩次(StewardTipCategory#category, StewardTipCategory#items各執(zhí)行一次)。
- 結(jié)束后創(chuàng)建一個(gè)com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer。
JavaBeanDeserializer:49行

JavaBeanDeserializer兩個(gè)重要屬性:
private final FieldDeserializer[] fieldDeserializers;
protected final FieldDeserializer[] sortedFieldDeserializers;
反序列化test.StewardTipCategory#items時(shí)fieldDeserializers的詳細(xì)信息。
com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializercom.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer#fieldValueDeserilizer(屬性值null,運(yùn)行時(shí)會(huì)根據(jù)fieldType獲取具體實(shí)現(xiàn)類)com.alibaba.fastjson.util.FieldInfo#fieldType(java.util.Map<java.lang.Integer, java.util.List<java.lang.String>>)

創(chuàng)建完成執(zhí)行
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)根據(jù)字段類型設(shè)置
com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer#fieldValueDeserilizer的具體實(shí)現(xiàn)類。
DefaultFieldDeserializer:34行

test.StewardTipCategory#items屬性的實(shí)際類型是List。
反序列化時(shí)根據(jù)C1構(gòu)造函數(shù)得到的fieldValueDeserilizer的實(shí)現(xiàn)類是com.alibaba.fastjson.parser.deserializer.MapDeserializer。
執(zhí)行
com.alibaba.fastjson.parser.deserializer.MapDeserializer#deserialze(com.alibaba.fastjson.parser.DefaultJSONParser, java.lang.reflect.Type, java.lang.Object)時(shí)報(bào)錯(cuò)。
MapDeserializer:228行

JavaBeanDeserializer:838行

java.lang.Class#getDeclaredConstructors返回[C2,C1]順序,反序列化時(shí)根據(jù)C2構(gòu)造函數(shù)得到的fieldValueDeserilizer的實(shí)現(xiàn)類是
com.alibaba.fastjson.parser.deserializer.ArrayListTypeFieldDeserializer,反序列化成功。
問(wèn)題解決
代碼
- 刪除C1構(gòu)造函數(shù),使用其他方式創(chuàng)建StewardTipCategory。
- 修改C1構(gòu)造函數(shù)參數(shù)名稱,類型,避免誤導(dǎo)Fastjson。
調(diào)試
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":["工藝類-提醒項(xiàng)-內(nèi)容1","工藝類-提醒項(xiàng)-內(nèi)容2"],"type":1},{"contents":["工藝類-疑問(wèn)項(xiàng)-內(nèi)容1"],"type":2}]}]}";
try {
JSONObject.parseObject(tip, StewardTip.class);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
總結(jié)
開(kāi)發(fā)過(guò)程中盡量遵照規(guī)范/規(guī)約,不要特立獨(dú)行
StewardTipCategory構(gòu)造函數(shù)C1方法簽名明顯不是一個(gè)很好的選擇,方法體除了屬性賦值,還做了一些額外的類型/數(shù)據(jù)轉(zhuǎn)換,也應(yīng)該盡量避免。
專業(yè)有深度
開(kāi)發(fā)人員對(duì)于使用的技術(shù)與框架要有深入的研究,尤其是底層原理,不能停留在使用層面。一些不起眼的事情可能導(dǎo)致不可思議的問(wèn)題:java.lang.Class#getDeclaredConstructors。
Fastjson
框架實(shí)現(xiàn)時(shí)要保持嚴(yán)謹(jǐn),報(bào)錯(cuò)信息盡可能清晰明了,StewardTipCategory反序列化失敗的原因在于,fastjson只檢驗(yàn)了屬性名稱,構(gòu)造函數(shù)參數(shù)個(gè)數(shù)而沒(méi)有進(jìn)一步校驗(yàn)屬性類型。
<<重構(gòu):改善既有代碼的設(shè)計(jì)>>提倡代碼方法塊盡量短小精悍,F(xiàn)astjson某些模塊的方法過(guò)于臃腫。
以上就是Fastjson反序列化隨機(jī)性失敗示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Fastjson反序列化隨機(jī)性的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot實(shí)現(xiàn)在一個(gè)模塊中引入另一個(gè)模塊
java 實(shí)現(xiàn)字節(jié)流和字節(jié)緩沖流讀寫文件時(shí)間對(duì)比
Java實(shí)現(xiàn)連接kubernates集群的兩種方式詳解
Java?Mybatis使用resultMap時(shí),屬性賦值順序錯(cuò)誤的巨坑
Java實(shí)現(xiàn)隨機(jī)驗(yàn)證碼功能實(shí)例代碼
Java基于余弦方法實(shí)現(xiàn)的計(jì)算相似度算法示例

