转载

Kotlin核心语法(八):注解与反射

博客主页

1. 声明并应用注解

1.1 应用注解

在kotlin中使用注解的方法与java一样,都是以@字符作为名字的前缀,并放在要注解声明的最前面。

来看下@Deprecated注解,在Kotlin中用replaceWith参数增强了它。

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

如果使用了 remove 函数, IDEA 不仅会提示应该使用哪个函数来代替它(这个例子中是 removeAt), 还会提供一个自动的快速修正。

Kotlin核心语法(八):注解与反射

注解只能拥有如下类型的参数: 基本数据类型、字符串、枚举、类引用、其他的注解类,以及前面这些类型的数组。

指定注解实参的语法与 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 规则用来创建文件和文件夹,并在测试结束后删除它们。

要指定一个规则,在 Java 中需要声明一个用@Rule 注解的 public 字段或者方法。 如果在你的kotlin测试类中只是用@Rule 注解了属性 folder ,你会得到一个JUnit 异常:“ The (???) 'folder' must be public." ( (???) 'folder ’必须是公有的)。

这是因为@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注解的实参的类

如果我的文章对您有帮助,不妨点个赞鼓励一下(^_^)

原文  https://segmentfault.com/a/1190000021250942
正文到此结束
Loading...