背景
最近开发的时候遇到一个问题,服务端的一个接口返回的 Json 中去掉了一个字段,客户端在使用 Gson
解析出的这个数据类的时候报空崩溃了。我在排查这个问题的时候发现这个数据类的这个字段是给了默认值的,但是解析的时候却赋值为 null
,而另外一个接口也有少了几个字段的情况,但是这个接口的数据类的这些变量默认值却是生效的。
为什么呢?
找不同
不知道有多少工程师在碰到问题时候会使用这个办法,努力的去对比一个运行正常的代码和出现bug的代码到底哪里不同?
然后,我真的发现了不同,如下示例:
data class Person(@SerializedName("name")val name:String = "no_name", @SerializedName("age")val age:Int = -1) data class Dog(@SerializedName("name")val name:String = "no_name", @SerializedName("age")val age:Int)
|
上面的两个数据类,当服务端下发的 Json 中没有 name
字段时,解析出的 Person
对象的 name
值是"no_name"
, 而同样的情况下,Dog
的 name
为 null
。
有什么不同呢?你应该也发现了,Person
类的 age
也设置了默认值,而 Dog
类只有 name
设置了默认值,会是这个原因吗?
经过修改,给 age
也设置上默认值,结果符合预期。
为什么
现在我们已经知道是什么地方导致了这个问题,那么深究一下为什么会出现这个问题。
如何深究?看源码。
看谁的源码?既然是用 Gson
解析的,那就先看看 Gson
是怎么解析的吧。
Gson 反序列化最后都会调用这个函数 fromJson()
,为了方便阅读省略了很多代码,关键点在于 typeAdapter.read(reader) 这行代码。
public <T> T fromJson(JsonReader reader, Type typeOfT) throws JsonIOException, JsonSyntaxException { reader.peek(); isEmpty = false; TypeToken<T> typeToken = (TypeToken<T>) TypeToken.get(typeOfT); TypeAdapter<T> typeAdapter = getAdapter(typeToken); T object = typeAdapter.read(reader); return object; }
|
点进去一看,是个接口,再一看,这个接口的子类贼多…
咋整? 很简单,我们可以依靠断点调试来帮我们找到此刻执行的是哪个子类中的代码,这也是一种阅读源码的技巧,在不清楚下游逻辑的情况下,断点跑一遍,把相关的类和方法理清楚。
断点可以发现这个类是:ReflectiveTypeAdapterFactory
,这段代码不复杂,也就分为两个部分
@Override public T read(JsonReader in) throws IOException { if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; } T instance = constructor.construct(); try { in.beginObject(); while (in.hasNext()) { String name = in.nextName(); BoundField field = boundFields.get(name); if (field == null || !field.deserialized) { in.skipValue(); } else { field.read(in, instance); } } } catch (IllegalStateException e) { throw new JsonSyntaxException(e); } catch (IllegalAccessException e) { throw new AssertionError(e); } in.endObject(); return instance; }
|
到这里该怎么继续查呢?也可以利用断点,断点可以发现,两种数据类,在构造 instance
实例的时候,Person
对象每个变量都有默认值,而Dog
对象的每个变量的值都是 null
或者 0
(基本数据类型)。
所以问题出在构造对象的时候!继续追可以追到一个类ConstructorConstructor
中,找到get
方法,同样省略掉了从缓存中获取的代码,只看第一次构造这个对象的代码。
public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) { final Type type = typeToken.getType(); final Class<? super T> rawType = typeToken.getRawType(); ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType); if (defaultConstructor != null) { return defaultConstructor; } return newUnsafeAllocator(type, rawType); }
|
有意思,所以在构造对象的时候会先获取无参构造函数来构造对象,否则用 unSafe 的方式去构造对象。
反射小知识:unSafe 方式去构造对象,会绕过构造函数,只会在堆中去分配一个对象实例
那么我们大胆预测一下:Person
类通过无参构造函数来构造对象,执行了对象的初始化代码,给变量设置了默认值,而Dog
类是通过 unSafe 的方式来构造的对象,没有给变量进行默认值设置,回到 adapter 解析json字段进行赋值的时候,json中有的字段就会覆盖变量的原始值,而没有的字段则不会覆盖,因此Dog
中的name
变量的值依然是null
!
经过断点验证推理是正确的。
那么问题来了
为什么 Person
和 Dog
的构造方式不一样呢?我们利用 Android Studio 的工具来把这两个类变成 java 文件
Tools > Kotlin > show kotlin byte code > Decompile
省略掉其他的代码,先看 Dog
:
@SerializedName("name") @NotNull private final String name; @SerializedName("age") private final int age; public Dog(@NotNull String name, int age) { Intrinsics.checkParameterIsNotNull(name, "name"); super(); this.name = name; this.age = age; } public Dog(String var1, int var2, int var3, DefaultConstructorMarker var4) { if ((var3 & 1) != 0) { var1 = "no_name"; } this(var1, var2); }
|
再看 Person
@SerializedName("name") @NotNull private final String name; @SerializedName("age") private final int age; public Person() { this((String)null, 0, 3, (DefaultConstructorMarker)null); } public Person(@NotNull String name, int age) { Intrinsics.checkParameterIsNotNull(name, "name"); super(); this.name = name; this.age = age; } public Person(String var1, int var2, int var3, DefaultConstructorMarker var4) { if ((var3 & 1) != 0) { var1 = "no_name"; } if ((var3 & 2) != 0) { var2 = -1; } this(var1, var2); }
|
Person
比 Dog
多一个无参的构造函数,这个构造函数调用的是 synthetic constructor, 即为变量赋上默认的值。而 Dog
因为有一个变量没有默认值,因此无法生成无参构造函数(必须要在构造的时候给它赋值)。
解决方案
到这里就真相大白了。所以这种默认值应该怎么去处理,简单的说就是要想办法让这个类能有一个无参构造函数。我们已经知道第一种方案了:
还有一种方案
class Person{ @SerializedName("name")var name:String = "no_name" @SerializedName("age")var age:Int }
|
这样,即便没有给 age
默认值,name
的默认值也一样会生效的。