背景知识:
Kotlin中有以下四种元注解(用来定义注解的注解):
- @Target:限定注解标记的目标(属性、方法、类、扩展等等)
- @Retention:限定注解是否存储到字节码文件中;在运行时通过反射是否可见(默认情况下以上两个条件均为真)
- @Repeatable:允许在同一个元素上重复使用同一个注解
- @MustBeDocumented:指定该注解是公有 API 的一部分,并且应该包含在生成的 API 文档中显示的类或方法的签名中。
在Kotlin中定义一个注解类,需要使用 annotation 关键字:
@Target(AnnotationTarget.PROPERTY)
annotation class Valid
实际操作
说明:此次实例是对Spring框架中的Value注解进行简单的实现。大致的运行过程可以概括成这样:在某个类中为它的属性添加@Value(value="key")注解,在配置文件中为注解中出现的关键字赋予相应的值。最终通过注解解析器将配置文件中的值注入到添加了注解的属性中。
第一步:定义注解
说明:我们所定义的是一个属性层级的注解,并且需要在运行时获取注解的相关信息,注解含有一个String类型的参数。最终注解定义的代码是这样子的:
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME) //这一行也可以省略
annotation class Value(val value:String)
第二步:对注解进行解析
说明:注解的定义十分的简单,就那么几行代码。但你要想让注解真正的起作用,你还需要对注解进行相应的解析才行。在解析注解的过程中会使用到大量和反射有关的代码,对反射的概念不熟悉的同学,先去看看反射要不然看接下来的这些代码会蛮吃力的。
class AnnotationExpression (val obj:Any){
fun expression(){
val clazz=obj::class
clazz.declaredMemberProperties.forEach { prop->
val mutableProp= try{
prop as KMutableProperty<*>
}catch (e:Exception){
null
} ?: return@forEach
mutableProp.annotations.forEach { annotation->
val propClassName=mutableProp.returnType.toString().removePrefix("kotlin.")
when(propClassName) {
in numtypeSet->mutableProp.setter.call(obj,
(readProp(annotation as Value) as kotlin.String).toNum(propClassName))
"String"->mutableProp.setter.call(obj,
(readProp(annotation as Value) as kotlin.String))
"Boolean"->mutableProp.setter.call(obj,
(readProp(annotation as Value) as kotlin.String).toBoolean())
}
}
}
}
通过KClass获取的KProperty1默认是不能被修改的,意味着你只能获取属性的值,而不能对其进行修改。所以在这里,我们对它进行了以下转换
prop as KMutableProperty<*>
因为可能出现使用该注解注释val变量的情况,在这里还进行了异常捕获,当发生异常时,直接跳过接下来的处理过程。
我们可以看到上面的代码中多次出现了 readProp 函数,在这里这个函数的作用是根据注解的信息,从配置文件中读取相应的数据。
private fun readProp(value:Value): Any? {
val prop=Properties()
prop.load( AnnotationExpression::class.java.getResource("app.properties").openStream())
return prop.get(value.value)
}
# app.properties
name="feint"
age=11
money=13.5
gender=true
由于直接从property中获取的类型可能会和使用了@Value注解的属性的类型不匹配,因此我们需要根据属性的类型对从配置中获取的类型进行转换。
在这里适配了,布尔型、字符串型以及数字型的数据。由于数字型的类别特别多(Int,Double,Byte等等),便专门为String扩展了一个 toNum 函数,它接受一个String类型的参数,表示类型的名称。具体的代码是下面这样子的:
fun String.toNum(className:String):Any{
val clazz=Class.forName("java.lang.${typeMap[className]}")
return clazz.getMethod("parse$className",String::class.java).invoke(null,this)
}
这个地方又有一个坑,我本来是想通过反射调用Kotlin的String类中类似toInt、toDouble的方法。可是,运行后竟然提示,Kotlin的内置类型目前对反射的支持还不完善。。。无奈只好使用Java中那些包装类的parse方法。
第三步:使用注解
说明:使用的过程也没啥好说的,直接上代码
class User{
@Value(value = "name")
lateinit var name:String
@Value(value = "age")
var age:Int=0
@Value(value = "money")
var money:Double=0.0
@Value(value = "gender")
var gender:Boolean=false
override fun toString(): String {
return "(name:$name; age:$age; money:$money; gender:${if(gender) "man" else "woman"})"
}
}
fun main(args: Array<String>) {
val user=User()
AnnotationExpression(user).expression()
println(user.toString())
}
源码地址(Github):注解练习