注册

Kotlin语法和 Gson 碰撞产生的空指针问题

1. 背景

  • Gson 作为 json 解析最有名的库,我们也在多处使用或借鉴其实现。但是 json解析本就存在很多问题,并且这些问题轻则导致数据丢失,重则直接崩溃,我们应该对他引起重视。在项目更新kotlin之后,更由于gson库是基于java设计的,进而引出了我们今天遇到的问题。

2. 问题

  • 当通过 kotlin 调用 Gson.fromJson(“json”, Class<T>) 解析 json,并且对象通过 kotlin 创建时,有可能在非空的字段解析出 null,例如使用下列 json 和 class 进行解析。
data class LowGsonData(
@SerializedName("name") var name: String,
@SerializedName("age") var age: Int,
@SerializedName("address") var address: String
)

data class LowGsonData(
@SerializedName("name") var name: String = "",
@SerializedName("age") var age?: Int = 0,
@SerializedName("address") var address: String = ""
)

class TestGson {
@Test
fun test() {
val json = "{\"name\":\"cong\",\"age\":11}"
// val json2 = "{\"name\":,\"age\":11}"
val testData = Gson().fromJson(json, LowGsonData::class.java)
println("testData: name = ${testData.name} age = ${testData.age} address = ${testData.address}")
}
}
  • 在我们使用上述两个LowGsonData对json进行解析时,我们关注一下 testData.address 会被解析为什么?

test_address_null.png

address 不是非空的吗?为什么这里address是空?这样在业务代码很容易因为kotlin的空安全检测,导致空指针问题!

3. 寻找原因

1.把kotlin data转为java

  • 因为 kotlin 最终都是转化成 java 字节码运行在虚拟机上的,所以我们先把这个类转为 java 代码方便我们看清这个对象的本质
public final class TestGsonData {
@SerializedName("name")
@NotNull
private String name;
@SerializedName("age")
private int age;
@SerializedName("address")
@NotNull
private String address;

public TestGsonData(@NotNull String name, int age, @NotNull String address) {
Intrinsics.checkNotNullParameter(name, "name");
Intrinsics.checkNotNullParameter(address, "address");
super();
this.name = name;
this.age = age;
this.address = address;
}
}
  • 看着好像没啥问题,调用这个构造函数依然能保证数据非空。那我们就需要继续分析gson是怎么构造出对象的?

2.分析Gson是如何构造对象的

  • Gson 的逻辑,一般都是根据读取到的类型,然后找对应的 TypeAdapter 处理,本例为普通自定义对象,所以会最终走到 ReflectiveTypeAdapterFactory.create 返回相应的 TypeAdapter。其中包含构造对象的方法 3 个:

(1)newDefaultConstructor :我们大部分对象都是通过这个地方创建的,获取无参的构造函数,如果能够找到,则通过 newInstance反射的方式构建对象。

private <T> ObjectConstructor<T> newDefaultConstructor(Class<? super T> rawType) {
try {
final Constructor<? super T> constructor = rawType.getDeclaredConstructor();
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
return new ObjectConstructor<T>() {
@SuppressWarnings("unchecked") // T is the same raw type as is requested
@Override public T construct() {
Object[] args = null;
return (T) constructor.newInstance(args);

// 省略了一些异常处理
};
} catch (NoSuchMethodException e) {
return null;
}
}

(2)newDefaultImplementationConstructor:都是一些集合类相关对象的逻辑。

(3)newUnsafeAllocator:通过 sun.misc.Unsafe 构造了一个对象,是用来访问 hidden API,以及获取一定的操作内存的能力。

public static UnsafeAllocator create() {
// try JVM
// public class Unsafe {
// public Object allocateInstance(Class<?> type);
// }
try {
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
Field f = unsafeClass.getDeclaredField("theUnsafe");
f.setAccessible(true);
final Object unsafe = f.get(null);
final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class);
return new UnsafeAllocator() {
@Override
@SuppressWarnings("unchecked")
public <T> T newInstance(Class<T> c) throws Exception {
assertInstantiable(c);
return (T) allocateInstance.invoke(unsafe, c);
}
};
} catch (Exception ignored) {
}

// try dalvikvm, post-gingerbread use ObjectStreamClass
// try dalvikvm, pre-gingerbread , ObjectInputStream

}
  • 现在我们已经知道了,当这个对象没有无参构造函数时,第一个方法不成立,最终会通过 unSafe 方式构建对象。虽然 gson 自身的设计,通过三种方式来保证对象创建成功很棒,但是这恰好在Unsafe构造中绕过了 kotlin 的空安全检查。

  • 所以 Unsafe 为啥没能符合空安全呢?

    因为 UnSafe 是直接获取内存中的值, String 对象在没有赋值时正好是 null,并且 json 里没有对应值,最后将不会覆盖他。

  • 好的真相大白了,那有什么改进方法吗?有,尽量满足第一个条件。

  • kotlin 的 data calss 只要有一个属性没有给初始值就不会生成无参构造方法。所以要想保证 gson 解析场景的非空性,我们应该给所有非可空属性附初始值。或者一开始就设置可空,并在业务代码中判空。

data class FullGsonData(
@SerializedName("name") var name: String = "",
@SerializedName("age") var age: Int = 0,
@SerializedName("address") var address: String = ""
)
  • 但是全都这么写吗?毕竟有些对象在业务中需要构造方法传入一些必传的值。那我就比较贪心,我既要又要还要。

  • 我的想法: 在聊天 elem 的场景,结合业务,封装一个工厂供业务构造对象。并在 data class 中继续保持非空构造。

  • 有没有其他好的想法?

    • 通过 kotlin 插件规避

4. 如何规避该问题:

经过调研我认为比较好的方式有:

1.引入noarg和allopen自动生成无参构造函数。

2.尝试对现有项目中使用的json解析库进行升级改造

如moshi,同时适配属性缺失、属性异常等在生产中可能会遇到的问题。

5. 参考:


作者:学不明白的聪
链接:https://juejin.cn/post/7258129183330779194
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册