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

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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容