从一个反序列化出发

Posted by rarnu on 01-20,2021

事情的起因是柿子同学发现了一个奇怪的问题,使用 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 中的一些反序列化的坑,你踩过了吗?