公司里的项目使用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。如果要避免这种情况的发生,在项目中应保证数据类的无参构造方法存在。
总结
- 不要破坏数据类的无参构造方法。
- 解析出来的实例其
基本数据类型的属性值不可能为null。 - 引用类型的属性应谨惕后端显式返null。