Kotlin + Gson 實現對 json 字段的非空檢查

用過 Kotlin 的小夥伴都已經知道 Kotlin 非空檢查寫法超級簡單。可是,處理 json 時,使用 gson 作解析封裝時,你會發現 Kotlin 的非空檢查不是那麼好用。java

先定義一個 json 實體類:json

data class KotlinData(
    var testNullable: String?,
    val testNooNull: String
)
複製代碼

兩個字段,一個能夠空,一個不能夠空。若是你直接建立這個對象,kt 保證了對非空的檢查和錯誤警告。接着,咱們看看使用 gson 封裝會怎樣。app

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

    assertNotNull(fromJson.testNullable)
複製代碼

上面的代碼結果可以正確封裝 KotlinData 對象, kt 的非空檢查就會欺騙你,而後空指針就找上門來。jvm

若是咱們想要規避這個問題,Gson 就須要稍微修改一下。自定義咱們 kt 的 TypeAdapter ,而後在 Adapter 的 read 方法中進行相關的非空判斷並拋出異常。write 方法就無論了。ide

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? ,感受這個 ? 很能說明一切。ui

接下來就是乾貨(C V)時間,如何運用到咱們的 gson 解析封裝中。this

Gson 優化

摒棄默認的 Gson() 建立方式,建立咱們自定義的 KotlinAdapterFactorygoogle

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†\b20B\b00¢J0HÆJ\t\f0HÆJ\r02\n\b02\b\b0HÆJ02\b0HÖJ\t0HÖJ02\b0J\t0HÖR0¢\b\n\bR0X†¢\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†\b20B\b00¢J0HÆJ\t\f0HÆJ\r02\n\b02\b\b0HÆJ02\b0HÖJ\t0HÖJ02\b0J\t0HÖR0¢\b\n\bR0X†¢\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.**{*;}
複製代碼

好了,又能夠開心の玩耍了。

相關文章
相關標籤/搜索