最近在梳理网络功能中遇到一个坑,当Gson解析生成一个Data Class时,如果字段缺失,引用类型都会为null。这样当客户端使用的时候,很容易产品空指针异常。非空类型原本的初衷是让程序更健壮,现在反而会使程序产生不可预估的后果。
解决这个问题的方式也可以很简单,将Data Class字段设置为可空类型,在业务层做大量安全判断。但这样的代码显然不够优雅,我们依然希望在网络层就能解决数据问题,让业务层不用频繁的去关心字段是否为空。
出于后端不信任原则,先来看看客户端可能遇到的两个问题
- json中某个字段缺失
- json中某个字段value为null
先来看看三个Case,不同的实现方案分别解析同一段Json数据。
第一个Case,实现一个UserBean,不包含任何默认参数。
data class UserBean(
val name: String,
val age: Int,
val notInJson: String,//json中不包含该字段
val inJsonIsNull: String//json中包含该字段但该字段值为null
)
val gson = Gson()
val json = """ {"name": "lhc","age": 18,"inJsonIsNull": null}"""
val data = gson.fromJson(json, UserBean::class.java)
//打印结果:UserBean(name=lhc, age=18, notInJson=null, inJsonIsNull=null)
checkNotNull(data.notInJson)//运行结果:抛出异常
checkNotNull(data.inJsonIsNull)//运行结果:抛出异常
复制代码
第二个Case,修改一下UserBean
,给予异常字段一个默认参数
data class UserBean(
val name: String,
val age: Int,
val notInJson: String = "DEFAULT",//json中不包含该字段
val inJsonIsNull: String = "DEFAULT"//json中包含该字段但该字段值为null
)
val gson = Gson()
val json = """ {"name": "lhc","age": 18,"inJsonIsNull": null}"""
val data = gson.fromJson(json, UserBean::class.java)
//打印结果:UserBean(name=lhc, age=18, notInJson=null, inJsonIsNull=null)
checkNotNull(data.notInJson)//运行结果:抛出异常
checkNotNull(data.inJsonIsNull)//运行结果:抛出异常
复制代码
第三个Case,给所有字段加上默认值
data class UserBean(
val name: String = "DEFAULT",
val age: Int = 10,
val notInJson: String = "DEFAULT",//json中不包含该字段
val inJsonIsNull: String = "DEFAULT"//json中包含该字段但该值字段为null
)
val gson = Gson()
val json = """ {"name": "lhc","age": 18,"inJsonIsNull": null}"""
val data = gson.fromJson(json, UserBean::class.java)
//UserBean(name=lhc, age=18, notInJson=DEFAULT, inJsonIsNull=null)
checkNotNull(data.notInJson)//运行结果:正常,notInJson不为nul
checkNotNull(data.inJsonIsNull)//运行结果:抛出异常
复制代码
经过测试我们得到一个结论。当DataClass所有字段(包括基础类型)都存在默认参数,Gson解析后,对于缺失的字段,该字段的值为默认值
那为什么会产生这样的差异?这里的具体原因还跟Kotlin默认参数的实现方案有关,就不展示只说一下结论:当DataClass所有字段(包括基础类型)都存在默认参数,编译器会为这个DataClass生成一个无参构造方法
那么到这里我们又有了两个新的疑问?
- 为什么无参构造方法能成功的赋予缺失字段默认值
- 为什么Gson解析并没有触发Kotlin的nullSafe检测
Gson解析流程
Gson解析内部通过反射创建示例,给字段赋值,直接ReflectiveTypeAdapterFactory
看起。
@Override public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
Class<? super T> raw = type.getRawType();
if (!Object.class.isAssignableFrom(raw)) {
return null; // it's a primitive!
}
ObjectConstructor<T> constructor = constructorConstructor.get(type);//(1)获取constructor
return new Adapter<T>(constructor, getBoundFields(gson, type, raw));
}
复制代码
看一下ConstructorConstructor#get
方法,用来返回一个构造器,主要用于实例化对象。主要有三种类型的构造器,无参构造器、容器类构造器、Unsafe构造器。如果没有找到匹配的构造器则会用Unsafe构造器兜底。
public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType);//(1)获取无参构造器
if (defaultConstructor != null) {
return defaultConstructor;
}
ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType);//(2)获取容器类的构造方法
if (defaultImplementation != null) {
return defaultImplementation;
}
// finally try unsafe
return newUnsafeAllocator(type, rawType);//通过Unsafe来生成对象
}
复制代码
ConstructorConstructor#newDefaultConstructor
内部获取了无参的构造方法,如果获取成功则返回,获取失败则返回null
private <T> ObjectConstructor<T> newDefaultConstructor(Class<? super T> rawType) {
//获取无参构造方法
final Constructor<? super T> constructor = rawType.getDeclaredConstructor();
if (!constructor.isAccessible()) {
accessor.makeAccessible(constructor);
}
return new ObjectConstructor<T>() {
@Override public T construct() {
Object[] args = null;
return (T) constructor.newInstance(args);
}
};
}
复制代码
当实体类没有无参构造函数,且不是容器类型,最终会命中ConstructorConstructor#newUnsafeAllocator
进行兜底。UnsafeAllocator内部通过非常规实例化对象方法allocateInstance
创建对象,该方法提供了通过Class对象就能创建实例的功能,不需要调用构造方法、初始化代码、JVM安全检查等,即使构造函数是private也能用此方法进行实例化
private <T> ObjectConstructor<T> newUnsafeAllocator(
final Type type, final Class<? super T> rawType) {
//返回ObjectConstructor,内部通过UnsafeAllocator创建实体类
return new ObjectConstructor<T>() {
private final UnsafeAllocator unsafeAllocator = UnsafeAllocator.create();
@Override public T construct() {
Object newInstance = unsafeAllocator.newInstance(rawType);
return (T) newInstance;
}
}
};
}
复制代码
当实例化完成,gson通过解析好的数据给每个字段赋值。gson会取出json中的每个key,去匹配
@Override public T read(JsonReader in) throws IOException {
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
BoundField field = boundFields.get(name);//每次取一个key,去解析好的实体类中查询是否包含该字段
if (field == null || !field.deserialized) {//如果实体类中不包含该字段则跳过,包含则赋值
in.skipValue();
} else {
field.read(in, instance);
}
}
}
@Override void read(JsonReader reader, Object value)
throws IOException, IllegalAccessException {
Object fieldValue = typeAdapter.read(reader);//获取字段值
if (fieldValue != null || !isPrimitive) {//如果字段不为null或者字段不是基础类型,则给字段赋值。因此如果返回的字段值为null,这里的赋值会覆盖原先的默认值
field.set(value, fieldValue);
}
}
复制代码
回到我们之前遇到的两个问题
-
为什么无参构造方法能成功的赋予缺失字段默认值
当实体类存在无参构造方法,Gson会通过反射调用无参构造方法来实例化对象,此时所有字段会被赋默认值。因此缺失字段获取的值为默认值。
-
为什么Gson解析并没有触发Kotlin的nullSafe检测
Gson给实体类中的每个值赋值通过反射实现,绕过了Data Class有参构造方法中的非空检测
如何解决
既然已经找到原因,该如何解决这两个问题。目前感觉想很好的容错依然需要两个方案的互补。
- 方案1:提供无参构造方法,给每个字段设置默认值
缺点:
1.模板代码太多,容易漏写
2.如果实体类中引用了另外一个实体类,该实体类也需要无参构造方法
目前在探索开发一个Android Studio插件提供给Data Class自动生成无参的构造方法,并且给每个字段设置默认值。这样能极大的减少模板代码编写。
也考虑过使用ASM实现该功能,给每个DataClass插入一个无参构造方法,但感觉这个方案比较繁琐,而且不能定制化默认参数,暂时不考虑
- 方案2:修改ReflectiveTypeAdapterFactory代码
缺点:
1.因为无法直接修改源码,只能复制一份修改一些必要的地方,通过反射替换。依赖Gson源码实现,升级Gson库需要兼容测试
2.能有效解决String、容器类为空问题,但得枚举需要兜底的类型。而且一旦引用其他实体类,如果依赖反射进行递归初始化,效率会比较低,同时也要求这个实体类有无参的构造方法。出于以上原因,我在进行兜底的时候只处理了字符串和部分容器类
修改BoundField#read
方法,原先逻辑为值为null,如果不是基本类型就赋值,改为为null不赋值
@Override
void read(JsonReader reader, Object value)throws IOException, IllegalAccessException {
Object fieldValue = typeAdapter.read(reader);
//值不为null,则进行赋值
if (fieldValue != null) {
this.hasSetValue = true;
field.set(value, fieldValue);
}
}
复制代码
在Adapter#read
方法返回前,插入兜底代码。目前只处理String、Map、List
//修改BoundField,方便后续做判断
static abstract class BoundField {
final String name;
final boolean serialized;
final boolean deserialized;
private final boolean isPrimitive;
public boolean hasSetValue = false;
final Field field;
}
/**
* 对于null字段进行兜底
*
* @param instance
*/
private void hookNullField(T instance) {
try {
for (BoundField boundField : boundFields.values()) {
//已初始化、基础数据类型、不序列化的字段不进行兜底初始化
if (boundField == null
|| !boundField.deserialized
|| boundField.hasSetValue
|| boundField.isPrimitive) {
continue;
}
Field field = boundField.field;
Class<?> type = field.getType();
if (type == String.class) {
field.set(instance, "");
} else if (type == List.class) {
field.set(instance, new ArrayList<>());
} else if (type == Map.class) {
field.set(instance, new HashMap<>());
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
复制代码