当Gson遇上data class

作者: 嘉伟咯 | 来源:发表于2022-11-01 23:03 被阅读0次

    当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就会调用无参构造函数去创建实例。

    解决思路

    了解完原理我们来看看怎么解决默认值无效的问题,下面有一些思路:

    1. 当需要使用默认值的时候全部成员变量都加上默认值
    2. 使用代码生成的方式创建InstanceCreator并注册到gson,在里面创建实例并预先填好默认值
    3. 改用对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))
    }
    
    

    相关文章

      网友评论

        本文标题:当Gson遇上data class

        本文链接:https://www.haomeiwen.com/subject/kxbdtdtx.html