當Gson遇上data class Gson解析流程 非空類型失效和構造函數不會被調用的原理 全部成員都有默認值的情況 解決思路

當Gson遇上kotlin data class,會發生一些很有意思的現象:

現象1: 非空類型失效

data class TestData(
    val a: String,
    val b: String
)
val data = Gson().fromJson("{}", TestData::class.java)
println("a:${data.a}, b:${data.b}") //輸出: a:null, b:null

現象2: 構造函數不會被調用

data class TestData(
    val a: String,
    val b: String
) {
    init {
        println("TestData init!!!") // 這一行代碼不會執行到
    }
}
val data = Gson().fromJson("{}", TestData::class.java)

現象3: 默認值失效

data class TestData(
    val a: String,
    val b: String = "bbb"
)
val data = Gson().fromJson("{\"a\":\"aaa\"}", TestData::class.java)
println("$data") //輸出: TestData(a=aaa, b=null)

現象4: 當全部成員都有默認值的時候默認值和構造函數又生效了

data class TestData(
    val a: String = "",
    val b: String = "bbb"
) {
    init {
        println("TestData init!!!") // 這一行代碼能執行到
    }
}
val data = Gson().fromJson("{\"a\":\"aaa\"}", TestData::class.java)
println("$data") //輸出: TestData(a=aaa, b=bbb)

Gson解析流程

要理解上面的現象我們先要了解Gson是怎樣工作的。

Gson解析json分兩步,創建對象實例和給成員變量賦值.

創建對象實例是通過ConstructorConstructor.get(TypeToken<T> typeToken)方法獲取到構造器去創建的:

public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
    final Type type = typeToken.getType();
    final Class<? super T> rawType = typeToken.getRawType();

    // 從instanceCreators中查找,我們可以用GsonBuilder.registerTypeAdapter指定某種類型的構造器,默認情況下instanceCreators是空的
    final InstanceCreator<T> typeCreator = (InstanceCreator<T>) instanceCreators.get(type);
    if (typeCreator != null) {
      return new ObjectConstructor<T>() {
        @Override public T construct() {
          return typeCreator.createInstance(type);
        }
      };
    }

    // 這裏還是在instanceCreators裏查找,只不過用rawType當key
    final InstanceCreator<T> rawTypeCreator =
        (InstanceCreator<T>) instanceCreators.get(rawType);
    if (rawTypeCreator != null) {
      return new ObjectConstructor<T>() {
        @Override public T construct() {
          return rawTypeCreator.createInstance(type);
        }
      };
    }

    // 查找一些特殊集合如EnumSet、EnumMap的構造器
    ObjectConstructor<T> specialConstructor = newSpecialCollectionConstructor(type, rawType);
    if (specialConstructor != null) {
      return specialConstructor;
    }

    // 通過rawType.getDeclaredConstructor()反射獲取類的無參構造函數
    FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, rawType);
    ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType, filterResult);
    if (defaultConstructor != null) {
      return defaultConstructor;
    }

    // 查找普通的Collection或者Map,如ArrayList、HashMap等的構造器
    ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType);
    if (defaultImplementation != null) {
      return defaultImplementation;
    }

    // 判斷類型是否可以實例化,例如接口和抽象類就不能實例化
    final String exceptionMessage = checkInstantiable(rawType);
    if (exceptionMessage != null) {
      return new ObjectConstructor<T>() {
        @Override public T construct() {
          throw new JsonIOException(exceptionMessage);
        }
      };
    }

    // 最後使用sun.misc.Unsafe去兜底創建實例
    if (filterResult == FilterResult.ALLOW) {
      return newUnsafeAllocator(rawType);
    } else {
      final String message = "Unable to create instance of " + rawType + "; ReflectionAccessFilter "
          + "does not permit using reflection or Unsafe. Register an InstanceCreator or a TypeAdapter "
          + "for this type or adjust the access filter to allow using reflection.";
      return new ObjectConstructor<T>() {
        @Override public T construct() {
          throw new JsonIOException(message);
        }
      };
    }
  }

獲取到對象的構造器,之後就能用它去創建對象實例,然後遍歷json字段查找對象是否有對應的成員變量,如果有就通過反射設置進去:

@Override
public T read(JsonReader in) throws IOException {
  if (in.peek() == JsonToken.NULL) {
    in.nextNull();
    return null;
  }

  // 通過ConstructorConstructor.get(TypeToken\<T\> typeToken)查詢的構造器創建實例對象
  A accumulator = createAccumulator();

  try {
    in.beginObject();
    // 遍歷json
    while (in.hasNext()) {
      String name = in.nextName();

      // 從對象的成員變量列表查詢是否有該字段
      BoundField field = boundFields.get(name);
      if (field == null || !field.deserialized) {
        // 對象沒有該成員變量則跳過
        in.skipValue();
      } else {
        // 對象有該成員變量則讀取json的值,通過反射設置給對象
        readField(accumulator, in, field);
      }
    }
  } catch (IllegalStateException e) {
    throw new JsonSyntaxException(e);
  } catch (IllegalAccessException e) {
    throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
  }
  in.endObject();
  return finalize(accumulator);
}

非空類型失效和構造函數不會被調用的原理

瞭解了Gson的解析流程之後我們再來看看問題1的data class對應的java代碼:

// kotlin代碼
data class TestData(
    val a: String,
    val b: String
)

// java對應的類
public final class TestData {
   private final String a;
   private final String b;
   ...
   public final String getA() {
      return this.a;
   }
   public final String getB() {
      return this.b;
   }
   ...
   public TestData(@NotNull String a, @NotNull String b) {
      Intrinsics.checkNotNullParameter(a, "a");
      Intrinsics.checkNotNullParameter(b, "b");
      super();
      this.a = a;
      this.b = b;
   }
   ...
   @NotNull
   public String toString() {
      return "TestData(a=" + this.a + ", b=" + this.b + ")";
   }
   ...
}

可以看到只有在構造函數裏面做了判空,但是它並沒有無參構造函數所以gson是通過Unsafe去兜底創建TestData實例的。Unsafe創建類的實例並不會調用到構造函數,所以就繞過類判空的步驟。

同理也能解釋現象2構造函數不會被調用的問題。

Unsafe

Unsafe是位於sun.misc包下的一個類,主要提供一些用於執行低級別、不安全操作的方法,如直接訪問系統內存資源、自主管理內存資源等,這些方法在提升Java運行效率、增強Java語言底層資源操作能力方面起到了很大的作用。 -- Java魔法類:Unsafe應用解析

我們可以通過下面的代碼創建TestData實例而不調用TestData的構造函數:

val unsafeClass = Class.forName("sun.misc.Unsafe")
val theUnsafe = unsafeClass.getDeclaredField("theUnsafe")
theUnsafe.isAccessible = true
val unsafe = theUnsafe.get(null)
val allocateInstance = unsafeClass.getMethod("allocateInstance", Class::class.java)
val testData = allocateInstance.invoke(unsafe, TestData::class.java) as TestData

data class 默認值的原理

接着我們繼續來看現象3默認值失效的問題,這裏會牽扯到data class默認值的原理,我們來看看對應的java代碼:

// kotlin代碼
data class TestData(
    val a: String,
    val b: String = "bbb"
)

// java對應的類
public final class TestData {
   private final String a;
   private final String b;

   ...

   public TestData(@NotNull String a, @NotNull String b) {
      Intrinsics.checkNotNullParameter(a, "a");
      Intrinsics.checkNotNullParameter(b, "b");
      super();
      this.a = a;
      this.b = b;
   }

   public TestData(String var1, String var2, int var3, DefaultConstructorMarker var4) {
      if ((var3 & 2) != 0) {
         var2 = "bbb";
      }

      this(var1, var2);
   }
   ...
}

可以看到,kotlin的默認參數並不是通過重載實現的,而是新增一個構造函數,用一個int的各個bit位來表示前面的參數是否需要設置成默認值。

// 例如下面這行kotlin代碼:
val testData = TestData("aaa")

// 對應的java代碼是這樣的:
TestData testData = new TestData("aaa", (String)null, 2, (DefaultConstructorMarker)null);

這樣做的好處在於只需要新建一個構造函數。用下面這種java傳統的函數重載來做,如果有很多的默認值的話需要創建很多的構造函數:

public final class TestData {
   ...

   public TestData(@NotNull String a, @NotNull String b) {
      ...
   }

   public TestData(String var1) {
      this(var1, "bbb");
   }
   ...
}

除了這個之外它也能比較方便的去支持任意位置的默認值.

到這裏我們也能理解現象3默認值失效的原因了,和前面的兩個現象一樣是因爲沒有調用到TestData的構造函數,所以就沒有賦默認值.

DefaultConstructorMarker

另外在生成的構造函數裏我們還看到了一個DefaultConstructorMarker參數:

public TestData(String var1, String var2, int var3, DefaultConstructorMarker var4)

這個參數會在kotlin自動生成的構造函數裏面出現,目的是爲了防止和我們自己定義的構造函數碰撞:

// kotlin代碼
data class TestData(
    val a: String,
    val b: String = "bbb"
) {
    constructor(a: String, b: String, i: Int) : this(a, b) {

    }
}

// 對應的java代碼
public final class TestData {
   private final String a;
   private final String b;

   public TestData(@NotNull String a, @NotNull String b) {
      ...
   }

   // 假設沒有DefaultConstructorMarker參數,下面的兩個構造函數就會撞車了
   public TestData(String var1, String var2, int var3, DefaultConstructorMarker var4) {
      ...
   }

   public TestData(@NotNull String a, @NotNull String b, int i) {
      Intrinsics.checkNotNullParameter(a, "a");
      Intrinsics.checkNotNullParameter(b, "b");
      this(a, b);
   }
   ...
}

全部成員都有默認值的情況

最後我們來分析下現象4當全部成員都有默認值的情況:

// kotlin代碼
data class TestData(
    val a: String = "",
    val b: String = "bbb"
) {
    init {
        println("TestData init!!!")
    }
}

// 對應的java代碼
public final class TestData {
   private final String a;
   private final String b;

   ...

   public TestData(@NotNull String a, @NotNull String b) {
      Intrinsics.checkNotNullParameter(a, "a");
      Intrinsics.checkNotNullParameter(b, "b");
      super();
      this.a = a;
      this.b = b;
      String var3 = "TestData init!!!";
      boolean var4 = false;
      System.out.println(var3);
   }

   public TestData(String var1, String var2, int var3, DefaultConstructorMarker var4) {
      if ((var3 & 1) != 0) {
         var1 = "";
      }

      if ((var3 & 2) != 0) {
         var2 = "bbb";
      }

      this(var1, var2);
   }

   public TestData() {
      this((String)null, (String)null, 3, (DefaultConstructorMarker)null);
   }

   ...
}

可以看到當所有成員都有默認值的時候,會生成無參構造函數,這樣的話Gson就會調用無參構造函數去創建實例。

解決思路

瞭解完原理我們來看看怎麼解決默認值無效的問題,下面有一些思路:

  1. 當需要使用默認值的時候全部成員變量都加上默認值
  2. 使用代碼生成的方式創建InstanceCreator並註冊到gson,在裏面創建實例並預先填好默認值
  3. 改用對kotlin支持更好的kotlinx.serialization或者moshi

kotlinx.serialization原理

kotlinx.serialization的原理在於@Serializable註解的data class對應的java代碼會多出一個$serializer類,它會記錄所有構造參數對應的json key,然後在解析出來的json裏面讀取出value去傳入構造函數:

// kotlin代碼
@Serializable
data class TestData(
    val a: String,
    val b: String = "aaa"
)

// java對應的類
public final class TestData {
   private final String a;
   private final String b;

   ...
   
   public static final class $serializer implements GeneratedSerializer {
      ...
      private static final SerialDescriptor $$serialDesc;
      
      static {
         TestData.$serializer var0 = new TestData.$serializer();
         INSTANCE = var0;
         PluginGeneratedSerialDescriptor var1 = new PluginGeneratedSerialDescriptor("me.linjw.demo.debugtool.TestData", (GeneratedSerializer)INSTANCE, 2);
         var1.addElement("a", false);
         var1.addElement("b", true);
         $$serialDesc = var1;
      }
      ...
      public TestData deserialize(@NotNull Decoder decoder) {
         Intrinsics.checkNotNullParameter(decoder, "decoder");
         SerialDescriptor var2 = $$serialDesc;
         int var4 = 0;
         String var5 = null;
         String var6 = null;
         Decoder decoder = decoder.beginStructure(var2);
         if (decoder.decodeSequentially()) {
            var5 = decoder.decodeStringElement(var2, 0);
            var6 = decoder.decodeStringElement(var2, 1);
            var4 = Integer.MAX_VALUE;
         } else {
            label19:
            while(true) {
               int var3 = decoder.decodeElementIndex(var2);
               switch(var3) {
               case -1:
                  break label19;
               case 0:
                  var5 = decoder.decodeStringElement(var2, 0);
                  var4 |= 1;
                  break;
               case 1:
                  var6 = decoder.decodeStringElement(var2, 1);
                  var4 |= 2;
                  break;
               default:
                  throw (Throwable)(new UnknownFieldException(var3));
               }
            }
         }

         decoder.endStructure(var2);
         return new TestData(var4, var5, var6, (SerializationConstructorMarker)null);
      }
      ...
   }
}

畢竟是kotlin官方的庫,能夠對生成的字節碼任意的做改動去實現。

moshi原理

moshi則比較委婉,通過kotlin的反射機制遍歷構造函數的參數,判斷有沒有可選參數,如果有的話就走callBy方法通過key-value map的方式傳入參數,如果沒有可選參數則通過vararg可變參數列表的方式順序傳入參數:

// Confirm all parameters are present, optional, or nullable.
var isFullInitialized = allBindings.size == constructorSize
for (i in 0 until constructorSize) {
  if (values[i] === ABSENT_VALUE) {
    when {
      constructor.parameters[i].isOptional -> isFullInitialized = false
      constructor.parameters[i].type.isMarkedNullable -> values[i] = null // Replace absent with null.
      else -> throw missingProperty(
        constructor.parameters[i].name,
        allBindings[i]?.jsonName,
        reader
      )
    }
  }
}

// Call the constructor using a Map so that absent optionals get defaults.
val result = if (isFullInitialized) {
  constructor.call(*values)
} else {
  constructor.callBy(IndexedParameterMap(constructor.parameters, values))
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章