當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就會調用無參構造函數去創建實例。
解決思路
瞭解完原理我們來看看怎麼解決默認值無效的問題,下面有一些思路:
- 當需要使用默認值的時候全部成員變量都加上默認值
- 使用代碼生成的方式創建InstanceCreator並註冊到gson,在裏面創建實例並預先填好默認值
- 改用對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))
}