美文网首页
Kotlin + Gson 实现对 json 字段的非空检查

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

作者: lovejjfg | 来源:发表于2019-07-30 09:42 被阅读0次

    用过 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.**{*;}
    

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

    相关文章

      网友评论

          本文标题:Kotlin + Gson 实现对 json 字段的非空检查

          本文链接:https://www.haomeiwen.com/subject/adnflctx.html