fastjson1.2.9解析帶abstract方法枚舉棧溢出剖析

一、案例背景

一個類裏面添加了枚舉,通過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代替。多用業界規範的框架。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章