Kotlin核心語法(八):註解與反射

博客主頁java

1. 聲明並應用註解

1.1 應用註解

在kotlin中使用註解的方法與java同樣,都是以@字符做爲名字的前綴,並放在要註解聲明的最前面。git

來看下@Deprecated註解,在Kotlin中用replaceWith參數加強了它。github

@Deprecated(message = "Use removeAt(index) instead .", replaceWith = ReplaceWith("removeAt(index)"))
fun remove(index: Int) { }

若是使用了 remove 函數, IDEA 不只會提示應該使用哪一個函數來代替它(這個例子中是 removeAt), 還會提供一個自動的快速修正。
json

註解只能擁有以下類型的參數: 基本數據類型、字符串、枚舉、類引用、其餘的註解類,以及前面這些類型的數組。segmentfault

指定註解實參的語法與 Java 有些微小的差異:數組

  • 要把一個類指定爲註解實參,在類名後加上 ::class : @MyAnnotation(MyClass::class)
  • 要把另外一個註解指定爲一個實參,去掉註解名稱前面的@ 例如,前面例中的 ReplaceWith 是一個註解,可是你把它指定爲 Deprecated 註解的實參時沒有用@
  • 要把一個數組指定爲一個實參,使用arrayOf函數:@RequestMapping(path = arrayOf ("I foo /bar 」))。若是註解類是在java中聲明的,命名爲 value 的形參按需自動地被轉換成可變長度的形參,因此不用arrayOf函數就能夠提供多個實參。

註解實參須要在編譯期就是己知的,因此你不能引用任意的屬性做爲實參。若是要把屬性看成註解實參使用,須要用 const 修飾符標記它,來告知編譯器這個屬性是編譯期常量:緩存

const val TEST_TIMEOUT = 100L

@Test(timeout = TEST_TIMEOUT)
fun testMethod() { }

1.2 註解目標

使用點目標聲明被用來講明要註解的元素。使用點目標被放在@符號和註解名稱之間,並用冒號和註解名稱隔開。網絡

// 單詞get致使註解@Rule被應用到了屬性的getter上

@get:Rule

舉一個例子:在JUnit中能夠指定一個每一個測試方法被執行以前都會執行的規則。標準的 TemporaryFolder 規則用來建立文件和文件夾,並在測試結束後刪除它們。app

要指定一個規則,在 Java 中須要聲明一個用@Rule 註解的 public 字段或者方法。 若是在你的kotlin測試類中只是用@Rule 註解了屬性 folder ,你會獲得一個JUnit 異常:「 The (???) 'folder' must be public." ( (???) 'folder ’必須是公有的)。ide

這是由於@Rule 被應用到了宇段上,而宇段默認是私有的。要把它應用 (公有的)getter 上,要顯式地寫出來,@get:Rule

class HasTempFolder {
    @get:Rule     // 註解的是getter,而不是屬性
    val folder = TemporaryFolder()

    @Test
    fun testUsingTempFolder() {
        val createdFile = folder.newFile("myfile.txt")
        val createdFolder = folder.newFolder("subFolder")
    }
}

Kotlin 支持的使用點目標的完整列表以下:

  • property——Java 的註解不能應用這種使用點目標
  • field——爲屬性生成的字段
  • get——屬性的 getter
  • set——屬性的 setter
  • receiver——擴展函數或者擴展屬性的接收者參數
  • param——構造方法的參數
  • setparam——屬性 setter 的參數
  • delegate——爲委託屬性存儲委託實例的字段
  • file——包含在文件中聲明的頂層函數和屬性的類

任何應用到 file 目標的註解都必須放在文件的頂層,放在 package 指令以前。@JvmName 是常見的應用到文件的註解之 ,它改變了對應類的名稱,如:

@file:JvmName("StringFuntions")
package com.example

和 Java 不同的是, Kotlin 容許你對任意的表達式應用註解,而不只是類和函數的聲明及類型。如:@Suppress註解,抑制編譯器警告。

fun test(list: List<*>) {
    @Suppress("UNCHECKED_CAST")
    val strings = list as List<String>
}

1.3 使用註解定製JSON序列化

註解的用法之一就是定製化對象的序列化。序列化就是一個過程,把對象轉換成能夠存儲或者在網絡上傳輸的二進制或者文本的表示法。它的逆向過程,反序列化,把這種表示法轉換回一個對象。

最多見的一種用來序列化的格式就JSON。目前有不少庫能夠把java對象序列化成JSON。括 Jackson(https: //github.com/FasterXML/jackson )和 GSON(https://github.com/google/gson)

接下來的例子中,使用一個名爲 JKid 的純 kotlin 庫。
http://github.com/yole/jkid

舉一個例子:序列化和反序列化Person 類的實例。serialize函數用來序列化

data class Person(
    val name: String,
    val age: Int
)

val person = Person("Alice", 20)
println(serialize(person))
// {"age": 20, "name": "Alice"}

deserialize函數用來反序列化

val json = """{"age": 20, "name": "Alice"}"""
println(deserialize<Person>(json))

還可使用註解來定製對象序列化和反序列化的方式。當把一個對象序列化成 JSON 的時候,默認狀況下這個庫嘗試序列化全部屬性,並使用屬性名稱做爲鍵。但使用註解@JsonExclude和@JsonName容許改變默認的行爲。

  • @JsonExclude 註解用來標記一個屬性,這個屬性應該排除在序列化和反序列化以外。
  • @JsonName 註解讓你說明表明這個屬性的(JSON )鍵值對之中的鍵應該是一個給定的字符串,而不是屬性的名稱。
data class Person(
    @JsonName("alias") val name: String,
    @JsonExclude val age: Int? = null
)

1.4 聲明註解

以 JKi 庫中的註解爲例:沒有參數的註解@JsonExclude

annotation class JsonExclude

在 class 關鍵宇以前加上了 annotation 修飾符。

有參數的註解@JsonName

annotation class JsonName(val name: String)

註解類的參數, val關鍵字是強制的。

// java
public @interface JsonName {
   String value();
}

若是把 Java 中聲明的註解應用到 Kotlin 元素上,必須對除了 value 之外的所
有實參使用命名實參語法,而 value 也會被 kotlin 特殊對待。

1.5 元註解:控制如何處理一個註解

與 Java 同樣,一個 Kotlin 註解類本身也能夠被註解。能夠應用到註解類上的註解被稱做元註解。標準庫中定義了一些元註解,它們會控制編譯器如何處理註解。如:@Target

@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude

@Target 元註解說明了註解能夠被應用的元素類型。

若是要聲明你本身的元註解,使用 ANNOTATION_CLASS 做爲目標:

@Target(AnnotationTarget.ANNOTATION_CLASS)
annotation class BindingAnnotation

@BindingAnnotation
annotation class MyBinding

在 Java 中還有一個重要的元註解:@Retention 它被用來聲明的註解是否會存儲到 .class 文件,以及在運行時是否能夠經過反射來訪問它。
kotlin 與 java 的默認行爲不一樣,kotlin註解擁有RUNTIME保留期。

1.6 使用類作註解參數

若是但願可以引用類做爲聲明的元數據。能夠經過聲明一個擁有類引用做爲形參的註解類來作到這一點。
在 JKid 庫中,@DeserializeInterface註解中,表示容許控制那些接口類型屬性的反序列化。

interface Company {
    val name: String
}

data class CompanyImpl(override val name: String) : Company

data class Person(
    val name: String,
    @DeserializeInterface(CompanyImpl::class) val company: Company
)

當 JKid 讀到一個 Person 類實例嵌套的 company 對象時,它建立並反序列化了 Companyimpl 的實例,把它存儲在 company 屬性中。

// DeserializeInterface聲明

import kotlin.reflect.KClass

@Target(AnnotationTarget.PROPERTY)
annotation class DeserializeInterface(val targetClass: KClass<out Any>)

KClass 是 Java 的java.lang.Class 類型在 Kotlin 中的對應類型。

若是你只寫出 KClass<Any>而沒有用 out 修飾符,就不能傳遞CompanyImpl::class 做爲實參: 惟一容許的實參將是 Any::class

1.7 使用泛型類作註解參數

默認狀況下, JKid 把非基本數據類型的屬性當成嵌套的對象序列化。可是你能夠改變這種行爲併爲某些值提供你本身的序列化邏輯。

@CustomSerializer 註解接收一個自定義序列化器類的引用做爲實參。這個序列化器類應該實現ValueSerializer 接口:

interface ValueSerializer<T> {
    fun toJsonValue(value: T): Any?
    fun fromJsonValue(jsonValue: Any?): T
}

@CustomSerializer 註解是如何聲明:

@Target(AnnotationTarget.PROPERTY)
annotation class CustomSerializer(val serializerClass: KClass<out ValueSerializer<*>>)

2. 反射:在運行時對kotlin對象進行自省

當在 Kotlin 中使用反射時,會有兩種不一樣的反射 API :
第一種是標準 Java 反射,定義在包 java.lang.reflect 中。由於 Kotlin 類會被編譯成普通
Java 字節碼, Java 反射 API 能夠完美地支持它們。
第二種是 kotlin 反射 API, 定義在包 kotlin.reflect中。

ext.kotlin_version = '1.3.41'

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
}

2.1 kotlin反射API:KClass、KCallable、KFunction和KProperty

Kotlin 反射 API 的主要入口就是 KClass ,它表明了一個類。KClass 對應的java.lang.class。

要在運行時取得一個對象的類,首先使用 javaClass 屬性得到它的 Java 類,這直接等價於 Java 中的 java.lang.Object.getClass() 。而後訪問該類的 .kotlin 擴展屬性,從 Java 切換到 kotlin 反射 API:

data class Person(
    val name: String,
    val age: Int
)

val person = Person("Alice", 20)
// 返回一個KClass<Person>的實例
val kClass = person.javaClass.kotlin;
println(kClass.simpleName)
// Person

kClass.memberProperties.forEach { println(it.name) }
//age
//name

KCallable 是函數和屬性的超接口。它聲明瞭 call 方法 容許你調用對應的函數或者對應屬性的 getter:

public actual interface KCallable<out R> : KAnnotatedElement {
     public fun call(vararg args: Any?): R
     // ...
}

如何經過反射使用 call 來調用一個函數:

fun foo(x: Int) = println(x)

val kFunction = ::foo
kFunction.call(23)
// 23

可使用 invoke 方法經過這個接口來調用函數。

val kFunction = ::foo
kFunction.invoke(12)
// 12

也能夠在一個 KProperty 實例上調用 call 方法, 它會調用該屬性的 getter 。可是屬性接口提供了一個更好的獲取屬性值的方式: get方法

頂層屬性表示爲 KPropertyO 接口的實例,它有一個無參數的 get 方法:

var counter = 0

fun main() {
    val kProperty = ::counter
    
    kProperty.setter.call(13) // 經過反射調用setter,把13做爲實參傳遞
    println(kProperty.get()) // 經過調用get獲取屬性的值
    // 13
}

一個成員屬性由KProperty1的實例表示,它有一個單參數的get方法。要訪問該屬性的值,必須提供你須要的值所屬的那個對象實例。

val person = Person("Alice", 20)

// KProperty<Person, Int>
// 第一個類型參數表示接收者的類型
//而第二個類型參數表明了屬性的類型
val memberProperty = Person::age

println(memberProperty.get(person))

只能使用反射訪問定義在最外層或者類中的屬性,而不能訪問函數的局部變量。

2.2 用反射實現對象序列化

來JKid 中序列化函數的聲明:

fun serialize(obj: Any): String

它經過一個StringBuilder 實例來構建 JSON 結果。實現放在 StringBuilder 的擴展函數中

private fun StringBuilder.serializeObject(obj: Any) {
    // 獲得對象的KClass,獲取類的全部屬性
    obj.javaClass.kotlin.memberProperties
            .filter { it.findAnnotation<JsonExclude>() == null }
            .joinToStringBuilder(this, prefix = "{", postfix = "}") {
                serializeProperty(it, obj)
            }
}

private fun StringBuilder.serializeProperty(
        prop: KProperty1<Any, *>, obj: Any
) {
    val jsonNameAnn = prop.findAnnotation<JsonName>()
    val propName = jsonNameAnn?.name ?: prop.name
    serializeString(propName)
    append(": ")

    val value = prop.get(obj)
    val jsonValue = prop.getSerializer()?.toJsonValue(value) ?: value
    serializePropertyValue(jsonValue)
}

標記成 private,以保證它不會在其餘地方使用。

2.3 用註解定製序列化

serializeObject函數是如何處理@JsonExclude、@JsonName和@CustomSerializer這些註解的呢?

先來看下@JsonExclude,這個註解容許你在序列化的時候排除某些屬性。那麼如何作到使用@JsonExclude 註解的屬性須要被過濾掉呢?
可使用 KClass 實例的擴展屬性 memberProperties ,來取得類的全部成員屬性。

而KAnnotatedElement 接口定義了屬性 annotations ,它是一個由應用到源碼中元素上的全部註解(具備運行時保留期)的實例組成的集合。由於 KProperty 繼承了 KAnnotatedElement ,能夠用 property.annotation這樣的寫法來訪問一個屬性的全部註解。

inline fun <reified T> KAnnotatedElement.findAnnotation(): T?
        = annotations.filterIsInstance<T>().firstOrNull()

private fun StringBuilder.serializeObject(obj: Any) {
    obj.javaClass.kotlin.memberProperties
            // 排除加了@JsonExclude註解的元素
            .filter { it.findAnnotation<JsonExclude>() == null }
            .joinToStringBuilder(this, prefix = "{", postfix = "}") {
                serializeProperty(it, obj)
            }
}

在來看下@JsonName註解,首先要是判斷@JsonName註解存不存在,還要關心它的實參:被註解的屬
性在 JSON 中應該用的名稱。

// 取得@JsonName註解的實例,若是它存在
val jsonNameAnn = prop.findAnnotation<JsonName>()
// 取得它的「name」實參或者備用的「prop.name」
val propName = jsonNameAnn?.name ?: prop.name

若是屬性沒有用@JsonName 註解, jsonNameAnn 就是 null ,而仍然須要使用 prop.name 做爲屬性在 JSON 中的名稱。若是屬性用@JsonName 註解了,你就會使用在註解中指定的名稱而不是屬性本身的名稱。

而後就是@CustomSerializer註解,它的實現基於 getSerializer 函數,該函數返回經過@CustomSerializer 註解註冊的 ValueSerializer 實例。

// 序列化屬性,支持自定義序列化器
@Target(AnnotationTarget.PROPERTY)
annotation class CustomSerializer(val serializerClass: KClass<out ValueSerializer<*>>)

fun KProperty<*>.getSerializer(): ValueSerializer<Any?>? {
    val customSerializerAnn = findAnnotation<CustomSerializer>() ?: return null
    val serializerClass = customSerializerAnn.serializerClass

    val valueSerializer = serializerClass.objectInstance
            ?: serializerClass.createInstance()
    @Suppress("UNCHECKED_CAST")
    return valueSerializer as ValueSerializer<Any?>
}

private fun StringBuilder.serializeProperty(
        prop: KProperty1<Any, *>, obj: Any
) {
    val jsonNameAnn = prop.findAnnotation<JsonName>()
    val propName = jsonNameAnn?.name ?: prop.name
    serializeString(propName)
    append(": ")

    val value = prop.get(obj)
    // 若是自定義序列化器存在就爲屬性使用它,不然像以前那樣使用屬性值
    val jsonValue = prop.getSerializer()?.toJsonValue(value) ?: value
    serializePropertyValue(jsonValue)
}

若是 KClass 表示的是一個普通的類,能夠經過調用 createInstance 來建立一個新的實例。這個函數和 java.lang.Class.newInstance 相似。
若是 KClass 表示的是一個單例實例,能夠經過objectInstance來訪問爲 object 建立的單例實例。

2.4 JSON解析和對象反序列化

先來看下反序列化的聲明:

inline fun <reified T: Any> deserialize(json: String): T

JKid 中的 JSON 反序列化器使用至關普通的方式實現,由三個主要階段組成:詞法分析器(一般被稱爲 lexer)、語法分析器或解析器,以及反序列化組件自己。

// 頂層反序列化函數

fun <T: Any> deserialize(json: Reader, targetClass: KClass<T>): T {
    // 建立一個 ObjectSeed 來存儲反序列化對象的屬性
    val seed = ObjectSeed(targetClass, ClassInfoCache())
    // 調用解析器並將輸入字符流 json 傳遞給它
    Parser(json, seed).parse()
    // 調用 spawn 函數來構建最終對象
    return seed.spawn()
}

看下ObjectSeed 的實現,它存儲了正在構造的對象的狀態 ObjectSeed 接收了一個目標類的引用和一個 classinfoCache 對象,該對象包含緩存起來的關於該類屬性的信息。這些緩存起來的信息稍後將被用於建立該類的實例。

// 序列化一個對象

class ObjectSeed<out T: Any>(
        targetClass: KClass<T>,
        override val classInfoCache: ClassInfoCache
) : Seed {
    // 緩存須要建立targetClass實例的信息
    private val classInfo: ClassInfo<T> = classInfoCache[targetClass]

    private val valueArguments = mutableMapOf<KParameter, Any?>()
    private val seedArguments = mutableMapOf<KParameter, Seed>()

    // 構建一個從構造方法參數到它們的值的映射
    private val arguments: Map<KParameter, Any?>
        get() = valueArguments + seedArguments.mapValues { it.value.spawn() }

    override fun setSimpleProperty(propertyName: String, value: Any?) {
        val param = classInfo.getConstructorParameter(propertyName)
        // 若是一個構造方法參數的值是簡單值,把它記錄下來
        valueArguments[param] = classInfo.deserializeConstructorArgument(param, value)
    }

    override fun createCompositeProperty(propertyName: String, isList: Boolean): Seed {
        val param = classInfo.getConstructorParameter(propertyName)
        // 若是有的話加載屬性DeserializeInterface註解的值
        val deserializeAs = classInfo.getDeserializeClass(propertyName)
        // 根據形參的類型建立一個ObjectSeed或者CollectionSeed
        val seed = createSeedForType(
                deserializeAs ?: param.type.javaType, isList)
        return seed.apply { seedArguments[param] = this }
    }

    // 傳遞實參map,建立targetClass實例做爲結果
    override fun spawn(): T = classInfo.createInstance(arguments)
}

ObjectSeed 構建了一個構造方法形參和它們的值之間的映射。

2.5 反序列化的最後一步:callBy()和使用反射建立對象

KCallable call 方法,不支持默認參數值,可使用另一個支持默認參數值的方法KCallable.callBy。

public actual interface KCallable<out R> : KAnnotatedElement {
     public fun callBy(args: Map<KParameter, Any?>): R
     // ...
}

根據值類型取得序列化器:

// 根據值類型取得序列化器

fun serializerForType(type: Type): ValueSerializer<out Any?>? =
        when (type) {
            Byte::class.java, Byte::class.javaObjectType -> ByteSerializer
            Short::class.java, Short::class.javaObjectType -> ShortSerializer
            Int::class.java, Int::class.javaObjectType -> IntSerializer
            Long::class.java, Long::class.javaObjectType -> LongSerializer
            Float::class.java, Float::class.javaObjectType -> FloatSerializer
            Double::class.java, Double::class.javaObjectType -> DoubleSerializer
            Boolean::class.java, Boolean::class.javaObjectType -> BooleanSerializer
            String::class.java -> StringSerializer
            else -> null
        }

對應的 ValueSerializer 實現會執行必要的類型檢查及轉換

// Boolean值的序列化器
object BooleanSerializer : ValueSerializer<Boolean> {
    override fun fromJsonValue(jsonValue: Any?): Boolean {
        if (jsonValue !is Boolean) throw JKidException("Expected boolean, was: $jsonValue")
        return jsonValue
    }

    override fun toJsonValue(value: Boolean) = value
}

ClassinfoCache 旨在減小反射操做的開銷。

// 混存的反射數據存儲

class ClassInfoCache {
    private val cacheData = mutableMapOf<KClass<*>, ClassInfo<*>>()

    @Suppress("UNCHECKED_CAST")
    operator fun <T : Any> get(cls: KClass<T>): ClassInfo<T> =
            cacheData.getOrPut(cls) { ClassInfo(cls) } as ClassInfo<T>
}

ClassInfo 類負責按目標類建立新實例井緩存必要的信息。

// 構造方法的參數及註解數據的緩存

class ClassInfo<T : Any>(cls: KClass<T>) {
    private val className = cls.qualifiedName
    private val constructor = cls.primaryConstructor
            ?: throw JKidException("Class ${cls.qualifiedName} doesn't have a primary constructor")

    private val jsonNameToParamMap = hashMapOf<String, KParameter>()
    private val paramToSerializerMap = hashMapOf<KParameter, ValueSerializer<out Any?>>()
    private val jsonNameToDeserializeClassMap = hashMapOf<String, Class<out Any>?>()

    init {
        constructor.parameters.forEach { cacheDataForParameter(cls, it) }
    }

    fun getConstructorParameter(propertyName: String): KParameter = jsonNameToParamMap[propertyName]
            ?: throw JKidException("Constructor parameter $propertyName is not found for class $className")

    fun deserializeConstructorArgument(param: KParameter, value: Any?): Any? {
        val serializer = paramToSerializerMap[param]
        if (serializer != null) return serializer.fromJsonValue(value)

        validateArgumentType(param, value)
        return value
    }

    fun createInstance(arguments: Map<KParameter, Any?>): T {
        ensureAllParametersPresent(arguments)
        return constructor.callBy(arguments)
    }
}

數據存儲在三個 map 中:

  • jsonNameToParamMap 說明了JSON 文件中的每一個鍵對應的形參
  • paramToSerializerMap 存儲了每一個形參對應的序列化器
  • jsonNameToDeserializeClassMap 存儲了指定爲@Deserializeinterface註解的實參的類

若是個人文章對您有幫助,不妨點個贊鼓勵一下(^_^)

相關文章
相關標籤/搜索