GSON解析生成实体类分析

391 阅读4分钟

公司里的项目使用GSON把后端返回的数据生成实体类,考虑到后端接口常常缺少字段,有时候也直接返回了null,避免出现线上事故,对返回的case做了测试。有些测试结果实在是意料之外,分享给大家。

测试代码

对KcHttpRequest文件中gson相关关键代码提取如下: (可直接拷贝下方代码到项目里做实验)

// 数据类
data class GsonBean(
        // 下面出现若发现实例中intValue
        // 为0说明是JVM默认值
        // 为1是咱们自定义的默认值
        val intValue: Int = 1,
        val intValueNullable: Int? = 1,
        val stringValue: String? = "自定义默认值",
        val classValue: Child = Child(intValue = 1)
)

// 无特殊用途,只为标识一个类
data class Child(
        val intValue: Int? = 1
)


object GsonTest {

    // 构建gson实例
    val gson: Gson by lazy {
        val builder = GsonBuilder()
        // 自定义String字段解析器,优先级高于默认解析器
        builder.registerTypeAdapterFactory(StringAdapterFactory())
        builder.create()
    }

    // main方法
    @JvmStatic fun main(args: Array<String>) {
        val data = """{"intValue": 0,"intValueNullable": 0,"stringValue": "字符串","classValue": {"intValue":2}}"""
        val result = gson.fromJson<GsonBean>(
                data,
                GsonBean::class.java
        )
        println(result.toString())
    }
}

Case1:正常情况

1.1 后端返的数据源

{
    "intValue": 3,
    "intValueNullable": 5,
    "stringValue": "测试",
    "classValue": {
        "intValue": 11
    }
}

1.2 解析生成的实例

GsonBean(
    intValue = 3, 
    intValueNullable = 5,
    stringValue = "测试",
    classValue = Child(intValue = 11)
)

1.3 解析过程

对上例,Gson在解析时先创建一个GsonBean实例(new GsonBean()),然后再拿json字符串中的value逐个替换实例中对应的属性值。如intValue生成实例时先被赋值为自定义默认值1,后才解析更改为真实值3。

Case2:后端字段缺失

2.1 后端返的数据源

{}

2.2 解析生成的实例

GsonBean(
    intValue = 1, 
    intValueNullable = 1, 
    stringValue = "自定义默认值",
    classValue = Child(intValue = 1)
)

2.3 结果分析

结果分析:实例中的属性值均为默认值。json字符串中没有数据,所以发生没有属性值替换,实例中均是默认值。

Case3:后端字段显式返回null

3.1 后端返的数据源

{
    "intValue": null,
    "intValueNullable": null,
    "stringValue": null,
    "classValue": null
}

3.2 解析生成的实例

GsonBean(
    intValue = 1, 
    intValueNullable = null, 
    stringValue = "", 
    classValue = null
)

3.3 结果分析

可以发现intValueNullable和classValue与json中的值一致;而intValue取的是自定义默认值,stringValue取的是空字符串。

3.3.1 intValue为什么取的是自定义默认值?

因为在Gson中会有逻辑判断,对于基本数据类型而言,只有在解析值非null的情况下才会去更改实例属性。判断逻辑见下方源码。

// 解析出value
Object fieldValue = typeAdapter.read(reader);
    // 解析值非空 || 属性非基本数据类型
   if (fieldValue != null || !isPrimitive) {
       field.set(value, fieldValue);
}
3.3.2 intValue和intValueNullable的结果为什么不一致?

因为intValue是Int型,而intValueNullable是Int?,自动装箱为Integer,不属于基本数据类型,所以会被赋值

null,在项目中实体类属性可优先考虑使用基本数据类型接收。

3.3.3 stringValue为什么取的是空字符串?

因为咱们自定义的StringAdpterFactory会在json值为null的情况下,把null转换为空字符串(开头代码第20行)。

3.3.4 classValue是非空类型,被赋值为null,为什么不抛出NPE?

设值是通过反射进行的,操作在native层,绕过了非空检测,我们使用到的时候才会报NPE。引用类型属性在声明时要注意有这样的情况,谨防后端显式返null

Case4: 数据类没有无参构造方法

gson优先通过反射使用无参构造方法生成实例,这里考虑一种不能通过无参构造方法生成实体类的情况,实体类结构更改如下:

data class GsonBean(
        val intValue: Int = 1,
        val intValueNullable: Int? = 1,
        val stringValue: String? = "自定义默认值",
        val classValue1: Child = Child(intValue = 2),
        val classValue2: Child
)

4.1 后端返的数据源(部分字段缺失)

{
    "classValue2": {
        "intValue": 3
    }
}

4.2 解析生成的实例

GsonBean(
    intValue = 0,
    intValueNullable = null, 
    stringValue = null, 
    classValue1 = null, 
    classValue2 = Child(intValue=3)
)

4.3 结果分析

生成实例过程如下:

通过newUnSafeAllocator生成的实例如下:

GsonBean(
    intValue = 0, 
    intValueNullable = null, 
    stringValue = null,
    classValue1 = null,
    classValue2 = null
)

由于后端只返回了classValue2字段,其它字段缺失,也就造成了其它属性值为null的情况(可空和不可空的属性全为null)。在调用的时候可能会抛NPE。如果要避免这种情况的发生,在项目中应保证数据类的无参构造方法存在。

总结

  1. 不要破坏数据类的无参构造方法。
  2. 解析出来的实例其基本数据类型的属性值不可能为null。
  3. 引用类型的属性应谨惕后端显式返null。