盒子
盒子
文章目录
  1. Gson解析流程
  2. 非空类型失效和构造函数不会被调用的原理
    1. Unsafe
    2. data class 默认值的原理
    3. DefaultConstructorMarker
  3. 全部成员都有默认值的情况
  4. 解决思路
    1. kotlinx.serialization原理
    2. moshi原理

当Gson遇上data class

当Gson遇上kotlin data class,会发生一些很有意思的现象:

现象1: 非空类型失效

1
2
3
4
5
6
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: 构造函数不会被调用

1
2
3
4
5
6
7
8
9
data class TestData(
val a: String,
val b: String
) {
init {
println("TestData init!!!") // 这一行代码不会执行到
}
}
val data = Gson().fromJson("{}", TestData::class.java)

现象3: 默认值失效

1
2
3
4
5
6
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: 当全部成员都有默认值的时候默认值和构造函数又生效了

1
2
3
4
5
6
7
8
9
10
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)方法获取到构造器去创建的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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字段查找对象是否有对应的成员变量,如果有就通过反射设置进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@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代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 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的构造函数:

1
2
3
4
5
6
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代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 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位来表示前面的参数是否需要设置成默认值。

1
2
3
4
5
// 例如下面这行kotlin代码:
val testData = TestData("aaa")

// 对应的java代码是这样的:
TestData testData = new TestData("aaa", (String)null, 2, (DefaultConstructorMarker)null);

这样做的好处在于只需要新建一个构造函数。用下面这种java传统的函数重载来做,如果有很多的默认值的话需要创建很多的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
public final class TestData {
...

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

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

除了这个之外它也能比较方便的去支持任意位置的默认值。

到这里我们也能理解现象3默认值失效的原因了,和前面的两个现象一样是因为没有调用到TestData的构造函数,所以就没有赋默认值.

DefaultConstructorMarker

另外在生成的构造函数里我们还看到了一个DefaultConstructorMarker参数:

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

这个参数会在kotlin自动生成的构造函数里面出现,目的是为了防止和我们自己定义的构造函数碰撞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 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当全部成员都有默认值的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 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去传入构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 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可变参数列表的方式顺序传入参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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))
}