1. 背景

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

2. 问题

  • 当通过 kotlin 调用 Gson.fromJson(“json”, Class<T>) 解析 json,而且目标通过 kotlin 创建时,有可能在非空的字段解析出 null,例如运用下列 jsonclass 进行解析。
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 会被解析为什么?

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

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 里没有对应值,最后将不会覆盖他。

  • 好的水落石出了,那有什么改善办法吗?有,尽量满意第一个条件。

  • kotlindata 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主动生成无参结构函数。

  • www.kotlincn.net/docs/refere…
  • /post/689303…

2.测验对现有项目中运用的json解析库进行升级改造

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

  • /post/684490…
  • /post/720957…

5. 参考:

  • blog.csdn.net/lmj62356579…
  • blog.csdn.net/java_cpp_/a…
  • www.kotlincn.net/docs/refere…