Kotlin + Gson 实现对 json 字段的非空检查

用过 Kotlin 的小伙伴都已经知道 Kotlin 非空检查写法超级简单。但是,处理 json 时,使用 gson 做解析封装时,你会发现 Kotlin 的非空检查不是那么好用。

先定义一个 json 实体类:

data class KotlinData(
    var testNullable: String?,
    val testNooNull: String
)

两个字段,一个可以空,一个不可以空。如果你直接创建这个对象,kt 保证了对非空的检查和错误警告。接着,我们看看使用 gson 封装会怎样。

    val fromJson = Gson().fromJson(
        "{\n" +
                "\t\"testNullable\":null,\n" +
                "\t\"testNooNull\":null\n" +
                "\t}"
        , KotlinData::class.java
    )

    assertNotNull(fromJson.testNullable)

上面的代码结果能够正确封装 KotlinData 对象, kt 的非空检查就会欺骗你,然后空指针就找上门来。

如果我们想要规避这个问题,Gson 就需要稍微修改一下。自定义我们 kt 的 TypeAdapter ,然后在 Adapter 的 read 方法中进行相关的非空判断并抛出异常。write 方法就不管了。

Kotlin 的非空标记

在 kt 的反射包中,提供了 isMarkedNullable 的属性,用于判断对应的 class 是否被标记为可空。

private fun nullCheck(kClass: KClass<KotlinData>) {
    try {
        kClass.annotations.forEach {
            Log.e("KTNullCheck", "annotation:$it")
        }
        kClass.declaredMemberProperties.forEach { prop ->
            prop.isAccessible = true
            Log.e("KTNullCheck", "prop:${prop},returnType>>>${prop.returnType}")
            val markedNullable = prop.returnType.isMarkedNullable
            Log.e("KTNullCheck", "${prop.name} is  nullable>>>>>>>>>>>:$markedNullable")
            Log.e("KTNullCheck", ">>>>>>>>>>>>>>>>>>>>>>>>>>>>")
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

这个方法最后的打印结果为:

com.lovejjfg.proguard E/KTNullCheck: prop:val com.lovejjfg.proguard.model.KotlinData.testNooNull: kotlin.String,returnType>>>kotlin.String
com.lovejjfg.proguard E/KTNullCheck: testNooNull is  nullable>>>>>>>>>>>:false
com.lovejjfg.proguard E/KTNullCheck: >>>>>>>>>>>>>>>>>>>>>>>>>>>>
com.lovejjfg.proguard E/KTNullCheck: prop:var com.lovejjfg.proguard.model.KotlinData.kotlin.String?: kotlin.String?,returnType>>>kotlin.String?
com.lovejjfg.proguard E/KTNullCheck: testNullable is  nullable>>>>>>>>>>>:true
com.lovejjfg.proguard E/KTNullCheck: >>>>>>>>>>>>>>>>>>>>>>>>>>>>

结果灰常完美,根据打印信息还可以看到,在标记为可空的字段 testNullable 上,其 returnTypekotlin.String? ,感觉这个 ? 很能说明一切。

接下来就是干货(C V)时间,如何运用到我们的 gson 解析封装中。

Gson 优化

摒弃默认的 Gson() 创建方式,创建我们自定义的 KotlinAdapterFactory

private val defaultGson = GsonBuilder()
    .registerTypeAdapterFactory(KotlinAdapterFactory())
    .create()

KotlinAdapterFactory 应该只对 kt 对象做非空判断等逻辑,那怎么区分是 kt 还是 Java 对象呢?毕竟最后他们都被转成字节码,脱了衣服,一个样儿。这里又要说到另外一个注解 Metadata
Kt 的元数据信息统统保存在这个注解头中。所以判断是否有这个注解,就能知晓是否是 kt 文件。

class KotlinAdapterFactory : TypeAdapterFactory {

    private fun Class<*>.isKotlinClass(): Boolean {
        return this.declaredAnnotations.any {
            // 只关心 kt 类型
            it.annotationClass.qualifiedName == "kotlin.Metadata"
        }
    }

    override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
        return if (type.rawType.isKotlinClass()) {
            val kClass = (type.rawType as Class<*>).kotlin
            val delegateAdapter = gson.getDelegateAdapter(this, type)
            KotlinAdapter<T>(delegateAdapter, kClass as KClass<T>)
        } else {
            null
        }
    }
}

class KotlinAdapter<T : Any>(
    private val delegateAdapter: TypeAdapter<T>,
    private val kClass: KClass<T>
) : TypeAdapter<T>() {

    override fun read(`in`: JsonReader?): T? {
        return delegateAdapter.read(`in`)?.apply {
            nullCheck(this)
        }
    }

    override fun write(out: JsonWriter?, value: T) {
        delegateAdapter.write(out, value)
    }

    private fun nullCheck(value: T) {
        kClass.declaredMemberProperties.forEach { prop ->
            prop.isAccessible = true
            if (!prop.returnType.isMarkedNullable && prop(value) == null)
                throw JsonParseException(
                    "Field: '${prop.name}' in Class '${kClass.java.name}' is marked nonnull but found null value"
                )
        }
    }
}

接着再添加一个测试代码:

@Test
fun testBuilder() {

    val fromJson = GsonBuilder()
        .registerTypeAdapterFactory(KotlinAdapterFactory())
        .create()
        .let {
            it.fromJson(json, KotlinData::class.java)
        }
    assertNotNull(fromJson.testNullable)
}

异常如期而至:

com.google.gson.JsonParseException: Field: 'testNooNull' in Class 'com.lovejjfg.proguard.model.KotlinData' is marked nonnull but found null value

at com.lovejjfg.proguard.gson.KotlinAdapter.nullCheck(KotlinAdapter.kt:35)
at com.lovejjfg.proguard.gson.KotlinAdapter.read(KotlinAdapter.kt:23)
at com.google.gson.Gson.fromJson(Gson.java:927)

好了,Kotlinjson 字段的非空检查完成。


如果就这么轻易搞定,那也不辛苦来码这篇文章。

混淆问题

调试的时候,到上面的确都 OK ,结果混淆 release 时,又出现各种问题。首先还是看看最上面 nullCheck(kClass: KClass<KotlinData>) 方法在混淆时候的打印情况。

结果是方法抛出异常:

 java.lang.IllegalStateException: No BuiltInsLoader implementation was found. 
 Please ensure that the META-INF/services/ is not stripped from your application 
 and that the Java virtual machine is not running under a security manager

在一番 Google 之后,更新混淆文件添加如下:

-keep class kotlin.reflect.jvm.internal.**{*;}

终于,这个方法成功打印出相关信息:

E/KTNullCheck: prop:var com.lovejjfg.proguard.a.a.a: kotlin.String!,returnType>>>kotlin.String!
E/KTNullCheck: a is  nullable>>>>>>>>>>>:false
E/KTNullCheck: >>>>>>>>>>>>>>>>>>>>>>>>>>>>
E/KTNullCheck: prop:val com.lovejjfg.proguard.a.a.b: kotlin.String!,returnType>>>kotlin.String!
E/KTNullCheck: b is  nullable>>>>>>>>>>>:false
E/KTNullCheck: >>>>>>>>>>>>>>>>>>>>>>>>>>>>

但是,这他么完全就是不正确的啊,所有的字段都成非空类型。kt 这是在开玩笑吗?混淆了至于这样吗?一番冷静之后,必须的思考为什么会这样呢,这个时候就必须反编译看一下 apk 最后生成的文件。

之前说过的 @Metadata 注解居然也被混淆,成了这个样子:

@m(a = {1, 1, 13}, b = {"\u0000(\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u000b\n\u0002\u0010\u000b\n\u0002\b\u0002\n\u0002\u0010\b\n\u0000\n\u0002\u0010\u0002\n\u0002\b\u0003\b�\b\u0018\u00002\u00020\u0001B\u0017\u0012\b\u0010\u0002\u001a\u0004\u0018\u00010\u0003\u0012\u0006\u0010\u0004\u001a\u00020\u0003¢\u0006\u0002\u0010\u0005J\u000b\u0010\u000b\u001a\u0004\u0018\u00010\u0003HÆ\u0003J\t\u0010\f\u001a\u00020\u0003HÆ\u0003J\u001f\u0010\r\u001a\u00020\u00002\n\b\u0002\u0010\u0002\u001a\u0004\u0018\u00010\u00032\b\b\u0002\u0010\u0004\u001a\u00020\u0003HÆ\u0001J\u0013\u0010\u000e\u001a\u00020\u000f2\b\u0010\u0010\u001a\u0004\u0018\u00010\u0001HÖ\u0003J\t\u0010\u0011\u001a\u00020\u0012HÖ\u0001J\u0010\u0010\u0013\u001a\u00020\u00142\b\u0010\u0015\u001a\u0004\u0018\u00010\u0000J\t\u0010\u0016\u001a\u00020\u0003HÖ\u0001R\u0011\u0010\u0004\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\u0006\u0010\u0007R\u001c\u0010\u0002\u001a\u0004\u0018\u00010\u0003X�\u000e¢\u0006\u000e\n\u0000\u001a\u0004\b\b\u0010\u0007\"\u0004\b\t\u0010\n¨\u0006\u0017"}, c = {"Lcom/lovejjfg/proguard/model/KotlinData;", "", "testNullable", "", "testNooNull", "(Ljava/lang/String;Ljava/lang/String;)V", "getTestNooNull", "()Ljava/lang/String;", "getTestNullable", "setTestNullable", "(Ljava/lang/String;)V", "component1", "component2", "copy", "equals", "", "other", "hashCode", "", "testData", "", "data", "toString", "app_release"})
// 转码之后
@m(a = {1, 1, 13}, b = {"(\n���\n��\n\n���\n�\b�\n���\n�\b�\n��\b\n\n���\n�\b�\b�\b�2�0�B��\b������0�������0�¢����J�������0�HÆ�J\t�\f��0�HÆ�J��\r��02\n\b�������0�2\b\b�����0�HÆ�J�����0�2\b������0�HÖ�J\t����0�HÖ�J�����0�2\b������0J\t����0�HÖ�R�����0�¢�\b\n��\b���R�������0�X��¢��\n��\b\b��\"�\b\t�\n¨��"}, c = {"Lcom/lovejjfg/proguard/model/KotlinData;", "", "testNullable", "", "testNooNull", "(Ljava/lang/String;Ljava/lang/String;)V", "getTestNooNull", "()Ljava/lang/String;", "getTestNullable", "setTestNullable", "(Ljava/lang/String;)V", "component1", "component2", "copy", "equals", "", "other", "hashCode", "", "testData", "", "data", "toString", "app_release"})

我们对比一下不混淆的注解:

@Metadata(bv = {1, 0, 3}, d1 = {"\u0000(\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u000b\n\u0002\u0010\u000b\n\u0002\b\u0002\n\u0002\u0010\b\n\u0000\n\u0002\u0010\u0002\n\u0002\b\u0003\b�\b\u0018\u00002\u00020\u0001B\u0017\u0012\b\u0010\u0002\u001a\u0004\u0018\u00010\u0003\u0012\u0006\u0010\u0004\u001a\u00020\u0003¢\u0006\u0002\u0010\u0005J\u000b\u0010\u000b\u001a\u0004\u0018\u00010\u0003HÆ\u0003J\t\u0010\f\u001a\u00020\u0003HÆ\u0003J\u001f\u0010\r\u001a\u00020\u00002\n\b\u0002\u0010\u0002\u001a\u0004\u0018\u00010\u00032\b\b\u0002\u0010\u0004\u001a\u00020\u0003HÆ\u0001J\u0013\u0010\u000e\u001a\u00020\u000f2\b\u0010\u0010\u001a\u0004\u0018\u00010\u0001HÖ\u0003J\t\u0010\u0011\u001a\u00020\u0012HÖ\u0001J\u0010\u0010\u0013\u001a\u00020\u00142\b\u0010\u0015\u001a\u0004\u0018\u00010\u0000J\t\u0010\u0016\u001a\u00020\u0003HÖ\u0001R\u0011\u0010\u0004\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\u0006\u0010\u0007R\u001c\u0010\u0002\u001a\u0004\u0018\u00010\u0003X�\u000e¢\u0006\u000e\n\u0000\u001a\u0004\b\b\u0010\u0007\"\u0004\b\t\u0010\n¨\u0006\u0017"}, d2 = {"Lcom/lovejjfg/proguard/model/KotlinData;", "", "testNullable", "", "testNooNull", "(Ljava/lang/String;Ljava/lang/String;)V", "getTestNooNull", "()Ljava/lang/String;", "getTestNullable", "setTestNullable", "(Ljava/lang/String;)V", "component1", "component2", "copy", "equals", "", "other", "hashCode", "", "testData", "", "data", "toString", "app_debug"}, k = 1, mv = {1, 1, 13})
// 转码之后
@Metadata(bv = {1, 0, 3}, d1 = {"(\n���\n��\n\n���\n�\b�\n���\n�\b�\n��\b\n\n���\n�\b�\b�\b�2�0�B��\b������0�������0�¢����J�������0�HÆ�J\t�\f��0�HÆ�J��\r��02\n\b�������0�2\b\b�����0�HÆ�J�����0�2\b������0�HÖ�J\t����0�HÖ�J�����0�2\b������0J\t����0�HÖ�R�����0�¢�\b\n��\b���R�������0�X��¢��\n��\b\b��\"�\b\t�\n¨��"}, d2 = {"Lcom/lovejjfg/proguard/model/KotlinData;", "", "testNullable", "", "testNooNull", "(Ljava/lang/String;Ljava/lang/String;)V", "getTestNooNull", "()Ljava/lang/String;", "getTestNullable", "setTestNullable", "(Ljava/lang/String;)V", "component1", "component2", "copy", "equals", "", "other", "hashCode", "", "testData", "", "data", "toString", "app_debug"}, k = 1, mv = {1, 1, 13})

默认的混淆之后, @Metadata 这个注解也被混淆了,所以,我们之前的 Kotlin 类型判断将失效。要解决这个问题,那就得把这个注解给保持住,最后的最后,还要注意,元数据中的字段等信息是没有被混淆的信息,所以,我们也应该保证 data 中每个字段不被混淆。

如果有对应的 model 没有被 keep ,app 会直接挂掉:

kotlin.reflect.jvm.internal.KotlinReflectionInternalError: 
No accessors or field is found for property val com.lovejjfg.proguard.a.KotlinData.testNooNull: kotlin.String

总的来说,在处理混淆是需要添加如下混淆规则:

-keep class kotlin.reflect.jvm.internal.**{*;}
-keep class kotlin.Metadata { *; }
# 所有需要走 gson 封装的 model 实体类需要保证 membername 不混淆 这里请根据实际情况制定自己的规则
-keepclassmembernames class com.lovejjfg.proguard.model.**{*;}

好了,又可以开心の玩耍了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容