事情的起因是柿子同学发现了一个奇怪的问题,使用 FastJson 对类进行反序列化时,产生了错误,如下代码所示:
data class TestEntity(
@JSONField(name = "updateTime", format = "yyyy/M/d HH:mm:ss")
var updateTime: LocalDateTime? = null
)
fun main(args: Array<String>) {
val json = """{"updateTime":"2020/6/27 15:59:44"}"""
val te = JSON.parseObject(json, TestEntity::class.java)
println(te)
}
粗看代码没什么问题,但是实际运行会产生一个 NullPointerException
,修改为以下代码又没有问题了:
class TestEntity {
@JSONField(format = "yyyy/M/d HH:mm:ss")
var updateTime: LocalDateTime? = null
}
这两种写法的区别仅仅在于,一个是 data class
,而另一个是普通的 class
,它们之间的区别到底在哪里呢?
我们需要借助反编译工具来查看编译结果,首先是 data class
的情况:
public final class TestEntity {
@Nullable
private LocalDateTime updateTime;
public TestEntity(@JSONField(name = "updateTime", format = "yyyy/M/d HH:mm:ss") @Nullable LocalDateTime updateTime) {
this.updateTime = updateTime;
}
@Nullable
public final LocalDateTime getUpdateTime() {
return this.updateTime;
}
public final void setUpdateTime(@Nullable LocalDateTime <set-?>) {
this.updateTime = <set-?>;
}
... ...
}
可以看到,data class
的注解在构造函数上,而内部的 updateTime
没有被注解。
再看一下普通的 class
的反编译结果,如下:
public final class TestEntity {
@JSONField(format = "yyyy/M/d HH:mm:ss")
@Nullable
private LocalDateTime updateTime;
@Nullable
public final LocalDateTime getUpdateTime() {
return this.updateTime;
}
public final void setUpdateTime(@Nullable LocalDateTime <set-?>) {
this.updateTime = <set-?>;
}
}
对于普通的 class
,注解在 updateTime
上,这就是造成 FastJson 无法反序列化的原因,因为 FastJson 无法对位于构造函数的字段作出正确的注入。
那么接下去我们就有一些选择,其一就是上面所述的,使用普通的 class
来进行 Entity 的定义,放弃使用 data class
。另一个选择是使用支持 data class
的库,比如说 Gson。
使用 Gson 的场景,也很简单,唯一不方便的是,需要对 LocalDataTime
的格式进行设置,那上面的代码可以进行一些改造,如下:
data class TestEntity(
var updateTime: LocalDateTime? = null
)
fun main(args: Array<String>) {
val json = """{"updateTime":"2020/6/27 15:59:44"}"""
val gson = GsonBuilder().registerTypeAdapter(LocalDateTime::class.java, JsonDeserializer { jsonElement, _, _ ->
LocalDateTime.parse(jsonElement.asJsonPrimitive.asString, DateTimeFormatter.ofPattern("yyyy/M/d HH:mm:ss"))
}).serializeNulls().create()
}
val te = gson.fromJson(json, TestEntity::class.java)
println(te)
在这里还需要注意一些事情,在排查问题的过程中,柿子把 Entity 写成如下形式:
class TestEntity {
@JSONField(format = "yyyy/M/d HH:mm:ss")
lateinit var updateTime: LocalDateTime
}
这个写法在工作场景下解决了问题,但是会带来一些隐患,例如当送入的 json 形如以下时:
{"updateTime":null}
这个时候将会产生无法初始化的异常,原因是在 TestEntity
内,定义的 updateTime
是允许延迟初始化,但是不允许为 null,当填入 null 值时,即会产生断言错误。
所以安全的写法是以 null 作为默认值:
class TestEntity {
@JSONField(format = "yyyy/M/d HH:mm:ss")
var updateTime: LocalDateTime? = null
}
既然提到了 lateinit var
的问题,不妨再来说一下对于 lateinit var
如何判断其是否已被初始化,我看过一些代码,都是直接使用该变量,并用 try 包围之,样例如下:
lateinit var te: TestEntity
... ...
val ut = try {
te.updateTime
} catch (th: Throwable) {
null
}
这个写法是不负责任的,try 应当被用在 常规流检查和常规错误检查 之中,而 te.updateTime
这样的场景显然更适合用 if
判断。具体的写法如下:
val ut = if (::te.isInitialized) te.updateTime else null
这里需要着重讲一下 ::
操作符,按官方文档(点击进入)所述,::
创建了成员引用或类引用,即是说 ::te
得到了 te
的引用,而 isInitialized
属性则是属于该引用的属性。在官方文档中,对属性引用也有详细讲述(点击进入),只是可能不太容易找到。
好了,在 Kotlin 中的一些反序列化的坑,你踩过了吗?