一、背景
一般我们在进行网络请求拿到返回结果之后,我们期望能够转化成对应的Java实体类,在这个转化过程中,可以使用自动解析的方式,也可以使用三方提供的工具类,比如Gson、FastJson等。
针对于Gson解析,可能都有遇到某个字段类型不匹配导致整个json解析失败的问题,这不是我们期望的,我们期望的是一个字段解析失败不影响其他字段的解析。
那这种情况下怎么办呢?
下面就是踩坑记录和解决方案
二、问题 & 解决方案
1. 绕过Kotlin非空判断问题
问题描述
data class User(
val name: String = "", //String类型
val age: Int, //Kotlin Int类型,不加?会转换成java的int类型,加?会转换成Integer类型
val bad: Boolean, //Kotlin Boolean类型,不加?会转换成java的boolean类型,加?会转换成Boolean类型
val relation: String = "",
val friends: User, //对象类型
val family: List<User> //数组类型
)
fun main() {
val gson = GsonBuilder().create()
val jsonStr = "{}"
val user = gson.fromJson(jsonStr, User::class.java)
println(user)
}
猜想一下上面打印出来的结果是什么?
答案是: User(name=null, age=0, bad=false, relation=null, friends=null, family=null)
可以看到我们声明的各种属性包含String类型,对象类型,数组类型,都是非空的,但是解析出来的结果却是null, 这并不符合我们的预期,如果这样的代码上线了,线上肯定崩溃无疑。
并且如果我们把age和bad属性添加?标识的话,我们将会得到下面的结果
User(name=null, age=null, bad=null, relation=null, friends=null, family=null)
所有的都变成null了。
那为什么会这样呢?针对这种情况,网上也能搜到对应文章说明
https://blog.csdn.net/lmj623565791/article/details/106631386
我们知道Kotlin data class类所有属性都赋初始值的时候,才会默认生成无参构造方法
由于上面的User类并没有所有的属性都赋初始值,最终导致生成的类没有无参的构造方法,而Kotlin data class的赋初始值的逻辑是在构造方法中实现的
Gson在没有找到默认构造方法时,它就直接通过Unsafe的方法,绕过了构造方法,直接构建了一个对象。
正是由于没走构造方法,所以默认值也不会被设置,最终数据的值就是Java中针对引用数据类型和基本数据类型对应的默认值了,即引用类型默认为null, 基本数据类型int 默认为0, boolean 默认为false
那在平常的开发过程中我们怎么避免此类问题发生呢?
解决方案
- data class的类所有属性都赋初始值,保证生成无参构造函数,使得默认值能够生效
- 对于引用数据类型尽量声明成可空类型,尽量确保程序可靠性
- 对于基本数据类型尽量声明成非可空类型,尽量减少内存消耗
所以针对上面的User类,我们应该尽量写成以下方式
//所有属性都赋初始值
@Keep //保持不被混淆
data class User(
val name: String? = "",
val age: Int = 0,
val bad: Boolean = false,
val relation: String? = "",
val friends: User? = null,
val family: List<User>? = null
)
这样的话,上面打印出来的结果为:
User(name=, age=0, bad=false, relation=, friends=null, family=null)
另外一种解决方案就是:不使用data class, 而是直接使用class, 然后在类里面去声明对应的属性
这种情况下对应属性必须赋初始值,并且会默认有无参的构造方法
class User {
private val name: String? = ""
private val age: Int = 0
private val bad: Boolean = false
private val relation: String? = ""
private val friends: User? = null
private val family: List<User>? = null
override fun toString(): String {
return "User(name=$name, age=$age, bad=$bad, relation=$relation, friends=$friends, family=$family)"
}
}
2. 类型不匹配导致解析失败问题
fun main() {
val gson = GsonBuilder().create()
val jsonStr = "{'name':'Coder','age':'aaa'}" // 1
// val jsonStr = "{'name':'Coder','friends':[]}" // 2
// val jsonStr = "{'name':'Coder','family':{}}" // 3
val user = gson.fromJson(jsonStr, User::class.java)
println(user)
}
针对上面三种情况,会发生什么样的结果?
可以看到这3中情况都会导致异常,程序不能正常运行
但是实际情况中这种情况又不可能避免,服务端那边使用的是弱类型语言的话,这种情况发生的概率极大,端上不能完全依靠服务端去保证,不然crash率就会蹭蹭往上涨。
解决方案
自定义 TypeAdapterFactory
gson 库会通过JsonReader对json对象的每个字段进项读取,当发现类型不匹配时抛出异常
那么我们就在它抛出异常的时候进行处理,让它继续不中断接着往下读取其他的字段就好了
具体代码为:
public class GsonTypeAdapterFactory implements TypeAdapterFactory {
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
final TypeAdapter<T> adapter = gson.getDelegateAdapter(this, type);
return new TypeAdapter<T>() {
@Override
public void write(JsonWriter out, T value) throws IOException {
adapter.write(out, value);
}
@Override
public T read(JsonReader reader) throws IOException {
try {
return adapter.read(reader);
} catch (Throwable e) {
e.printStackTrace();
if (reader.hasNext()) {
JsonToken peek = reader.peek();
if (peek == JsonToken.NAME) {
reader.nextName();
} else {
reader.skipValue();
}
}
return null;
}
}
};
}
}
fun main() {
val gson = GsonBuilder().registerTypeAdapterFactory(GsonTypeAdapterFactory()).create() //gson创建的时候registerTypeAdapterFactory
val jsonStr = "{'name':'Coder','age':'aaa'}" // 1
// val jsonStr = "{'name':'Coder','friends':[]}" // 2
// val jsonStr = "{'name':'Coder','family':{}}" // 3
val user = gson.fromJson(jsonStr, User::class.java)
println(user)
}
这种写法,上面三种情况都不会报错,并且最终解析的结果为:
User(name=Coder, age=0, bad=false, relation=, friends=null, family=null)