一、案例背景
一個類裏面添加了枚舉,通過fastjson把該類解析爲json的過程報錯。
二、 案例現場
定義一個帶abstract方法的枚舉
public enum OpenSourceEnum {
BILIBILI("bilibili") {
@Override
public Integer getUid(Integer mid, String openId) {
return mid;
}
},
MISSEVAN("missevan") {
@Override
public Integer getUid(Integer mid, String openId) {
return Integer.valueOf(openId);
}
};
public final String code;
OpenSourceEnum(String code) {
this.code = code;
}
public String getCode() {
return code;
}
public abstract Integer getUid(Integer mid, String openId);
}
然後解析爲json
com.alibaba.fastjson.JSON.toJSON(OpenSourceEnum.BILIBILI)
遞歸死循環造成棧溢出
Exception in thread "main" com.alibaba.fastjson.JSONException: toJSON error
at com.alibaba.fastjson.JSON.toJSON(JSON.java:651)
at com.alibaba.fastjson.JSON.toJSON(JSON.java:570)
at com.alibaba.fastjson.JSON.toJSON(JSON.java:642)
at com.alibaba.fastjson.JSON.toJSON(JSON.java:570)
at com.alibaba.fastjson.JSON.toJSON(JSON.java:624)
at com.alibaba.fastjson.JSON.toJSON(JSON.java:570)
at com.alibaba.fastjson.JSON.toJSON(JSON.java:642)
at com.alibaba.fastjson.JSON.toJSON(JSON.java:570)
三、案例剖析
3.1 枚舉的實質
JDK 1.5中增加了enum關鍵字, 但是Class文件常量池的CONSTANT_Class_info類型常量並
沒有發生任何語義變化, 仍然是代表一個類或接口的符號引用, 沒有加入枚舉, 也沒有增
加過"CONSTANT_Enum_info"之類的“ 枚舉符號引用” 常量。 所以使用enum關鍵字定義常
量, 雖然從Java語法上看起來與使用class關鍵字定義類、 使用interface關鍵字定義接口
是同一層次的, 但實際上這是由Javac編譯器做出來的假象, 從字節碼的角度來看, 枚舉
僅僅是一個繼承於java.lang.Enum、 自動生成了values()和valueOf()方法的普通Java類
而已。
——《深入理解Java虛擬機》
3.2 帶abstract方法枚舉反編譯
枚舉編譯爲class後可以發現除OpenSourceEnum外,還多了兩個內部子類,內部子類class文件通常使用"父類名+$+xxx" 命名格式。
通過 javap -c OpenSourceEnum.class 查看字節碼 , 可以看到 OpenSourceEnum 聲明爲 abstract class OpenSourceEnum extends Enum,
就是一個普通抽象類。
public abstract class com.alibaba.fastjson.OpenSourceEnum extends java.lang.Enum<com.alibaba.fastjson.OpenSourceEnum> {
public static final com.alibaba.fastjson.OpenSourceEnum BILIBILI;
public static final com.alibaba.fastjson.OpenSourceEnum MISSEVAN;
public final java.lang.String code;
public static com.alibaba.fastjson.OpenSourceEnum[] values();
Code:
0: getstatic #2 // Field $VALUES:[Lcom/alibaba/fastjson/OpenSourceEnum;
3: invokevirtual #3 // Method "[Lcom/alibaba/fastjson/OpenSourceEnum;".clone:()Ljava/lang/Object;
6: checkcast #4 // class "[Lcom/alibaba/fastjson/OpenSourceEnum;"
9: areturn
public static com.alibaba.fastjson.OpenSourceEnum valueOf(java.lang.String);
Code:
0: ldc #5 // class com/alibaba/fastjson/OpenSourceEnum
2: aload_0
3: invokestatic #6 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #5 // class com/alibaba/fastjson/OpenSourceEnum
9: areturn
public java.lang.String getCode();
Code:
0: aload_0
1: getfield #8 // Field code:Ljava/lang/String;
4: areturn
public abstract java.lang.Integer getUid(java.lang.Integer, java.lang.String);
com.alibaba.fastjson.OpenSourceEnum(java.lang.String, int, java.lang.String, com.alibaba.fastjson.OpenSourceEnum$1);
Code:
0: aload_0
1: aload_1
2: iload_2
3: aload_3
4: invokespecial #1 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
7: return
static {};
Code:
0: new #9 // class com/alibaba/fastjson/OpenSourceEnum$1
3: dup
4: ldc #10 // String BILIBILI
6: iconst_0
7: ldc #11 // String bilibili
9: invokespecial #12 // Method com/alibaba/fastjson/OpenSourceEnum$1."<init>":(Ljava/lang/String;ILjava/lang/String;)V
12: putstatic #13 // Field BILIBILI:Lcom/alibaba/fastjson/OpenSourceEnum;
15: new #14 // class com/alibaba/fastjson/OpenSourceEnum$2
18: dup
19: ldc #15 // String MISSEVAN
21: iconst_1
22: ldc #16 // String missevan
24: invokespecial #17 // Method com/alibaba/fastjson/OpenSourceEnum$2."<init>":(Ljava/lang/String;ILjava/lang/String;)V
27: putstatic #18 // Field MISSEVAN:Lcom/alibaba/fastjson/OpenSourceEnum;
30: iconst_2
31: anewarray #5 // class com/alibaba/fastjson/OpenSourceEnum
34: dup
35: iconst_0
36: getstatic #13 // Field BILIBILI:Lcom/alibaba/fastjson/OpenSourceEnum;
39: aastore
40: dup
41: iconst_1
42: getstatic #18 // Field MISSEVAN:Lcom/alibaba/fastjson/OpenSourceEnum;
45: aastore
46: putstatic #2 // Field $VALUES:[Lcom/alibaba/fastjson/OpenSourceEnum;
49: return
}
查詢內部子類OpenSourceEnum$1.class 字節碼,可以看到內部子類的聲明:OpenSourceEnum$1 extends OpenSourceEnum,子類
OpenSourceEnum$1 與父類OpenSourceEnum是繼承關係。
final class com.alibaba.fastjson.OpenSourceEnum$1 extends com.alibaba.fastjson.OpenSourceEnum {
com.alibaba.fastjson.OpenSourceEnum$1(java.lang.String, int, java.lang.String);
Code:
0: aload_0
1: aload_1
2: iload_2
3: aload_3
4: aconst_null
5: invokespecial #1 // Method com/alibaba/fastjson/OpenSourceEnum."<init>":(Ljava/lang/String;ILjava/lang/String;Lcom/alibaba/fastjson/OpenSourceEnum$1;)V
8: return
public java.lang.Integer getUid(java.lang.Integer, java.lang.String);
Code:
0: aload_1
1: areturn
}
3.3 沒有地處理枚舉
查看com.alibaba.fastjson.JSON.toJSON(java.lang.Object) 源碼,發現有一處判斷對象是否爲枚舉的邏輯,但沒生效,繼續往下走陷入了死循
環。
if (clazz.isEnum()) {
return ((Enum<?>) javaObject).name(); // 這個地方應該return,卻沒有
}
查看java.lang.Class.isEnum源碼,關鍵代碼爲取父類的class類型跟Enum比對,可以看到註釋,isEnum方法枚舉只有在直接繼承了Enum的情況
下才爲true
public boolean isEnum() {
// An enum must both directly extend java.lang.Enum and have
// the ENUM bit set; classes for specialized enum constants
// don't do the former.
return (this.getModifiers() & ENUM) != 0 &&
this.getSuperclass() == java.lang.Enum.class;
}
枚舉的子類調用isEnum返回的不是true
OpenSourceEnum.BILIBILI.getClass().isEnum()==false
因爲OpenSourceEnum.BILIBILI是OpenSourceEnum的子類
子類在isEnum()裏面調用getSuperclass返回的是OpenSourceEnum.class
顯然OpenSourceEnum.class不等於java.lang.Enum.class
3.4 棧溢出的地方
由於 com.alibaba.fastjson.JSON.toJSON裏面的clazz.isEnum()沒有正確判斷枚舉,邏輯接着往下走,到了如下代碼
List<FieldInfo> getters = TypeUtils.computeGetters(clazz, clazz.getAnnotation(JSONType.class), null, false); // 通過反射拿到對象的Get方法。
JSONObject json = new JSONObject(getters.size());
for (FieldInfo field : getters) {
Object value = field.get(javaObject); // 基於反射通過Method方法拿到該Method返回的值,以便遞歸解析爲json
Object jsonValue = toJSON(value); // 遞歸死循環的地方
json.put(field.name, jsonValue);
}
TypeUtils.computeGetters工具類會取對象類上面所有的Method,然後通過for循環拿到所有getX Method對應的值。
自然會拿到拿到子類枚舉OpenSourceEnum.BILIBILI的java.lang.Enum.getDeclaringClass的方法,及getDeclaringClass的返回值。
查看java.lang.Enum.getDeclaringClass源碼,取的是枚舉對象的父類,如果枚舉對象直接繼承了 java.lang.Enum,返回該枚舉對象自身的class.
public final Class<E> getDeclaringClass() {
Class<?> clazz = getClass();
Class<?> zuper = clazz.getSuperclass();
return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
}
最終會遞歸窮舉解析getDeclaringClass對象上面的各種Get方法。JSON.toJSON每次都會根據傳參對象,取該對象上面的get方法。先調用Class.getClass然後通過反射取Class對象上面的所有get方法,其中getClasses與getClass這兩個get方法返回的也是Class,toJson裏面的判斷條件對Class對象都不生效,遞歸Class無窮無盡,造成棧溢出。
以下是元兇
JSON.toJSON(OpenSourceEnum.BILIBILI.getDeclaringClass())
3.5 一行代碼重現問題
JSON.toJSON(String.class)
一句話總結:fastjson 1.2.9沒能很好處理枚舉子類,當傳參對象爲 Object.class的時候容錯性不足。
四、問題的解決
先升級fastjson爲最新穩定版
新代碼不用fastjson,使用Google Gson代替。多用業界規範的框架。