Kotlin学习(12)元编程、注解与反射

1. 元编程

元编程就是 关于编程的编程。本质上是一种对源码本身进行高层次抽象的编码技术。

在编程中,注解、反射就很好的促进了元编程
元编程有两种方式:

  • 通过应用程序接口(API)来暴露运行时系统的内部信息
  • 在运行时动态执行包含变成编程命令的字符串

注解是把元数据信息直接写在了源代码中,而不是保存在外部文件中。

注解将元数据附加到代码中。而反射可以在运行时把代码中的注解元数据获取到,并在目标代码执行之前进行动态代理,实现业务逻辑的动态注入,这其实就是AOP的核心思想----通过运行期动态代理实现不修改源码的情况下,给程序动态添加新功能的一种技术。

2. 注解

Kotlin的注解跟Java的注解完全兼容。

2.1 声明注解
在Kotlin中声明注解使用 annotation class关键字,例如,我们要声明两个注解 Run 和 TestCase 如下:

   @Target(
        AnnotationTarget.CLASS,
        AnnotationTarget.FUNCTION,
        AnnotationTarget.VALUE_PARAMETER,
        AnnotationTarget.EXPRESSION
    )
    @Retention(AnnotationRetention.SOURCE)
    @Repeatable
    @MustBeDocumented
    annotation class TestCase(val id: String)

    
    @Target(AnnotationTarget.CLASS,
        AnnotationTarget.FUNCTION,
        AnnotationTarget.VALUE_PARAMETER,
        AnnotationTarget.EXPRESSION)
    @Retention(AnnotationRetention.SOURCE)
    @Repeatable
    @MustBeDocumented
    annotation class Run

从这个关键字上可以看出注解也是一种class,编译器同样可以对注解类型在编译器进行类型检查。

自定义的注解中使用的注解,称之为元注解。通过向注解类添加元注解的方式来指定其他属性。元注解说明如表:

元注解名称 功能说明
@Target 指定这个注解可被用于哪些元素 ( 这些元素定义在kotlin.annotation.AnnotationTarget 枚举类中。它们是:类 CLASS, 注解类 ANNOTATION_CLASS,泛型参数 TYPE_PARAMETER,函数 FUNCTION, 属性 PROPERTY, 用于描述域成员变量的 FIELD,局部变量 LOCAL_VARIABLE,VALUE_PARAMETER,CONSTRUCTOR,PROPERTY_GETTER,PROPERTY_SETTER, 用于描述类、接口(包括注解类型) 或enum声明的 TYPE, 表达式 EXPRESSION,文件 FILE,类型别名TYPEALIAS等
@Retention 指定这个注解的信息是否被保存到编译后的 class 文件中, 以及在运行时是否可以通过反射访问到它, 可取的枚举值有3个,分别是: SOURCE (注解数据不存储在二进制输出),BINARY(注解数据存储在二进制输出中, 但反射不可见), RUNTIME(注解数据存储在二进制输出中, 可用于反射 (默认值 )
@Repeatable 允许在单个元素上多次使用同一个注解
@MustBeDocumented 表示这个注解是公开 API 的一部分, 在自动产生的 API 文档的类或者函数签名中, 应该包含这个注解的信息。

2.2 使用注解
上面我们声明了Run注解,它可以使用在 CLASS、FUNCTION、VALUE_PARAMETER和EXPRESSION上,我们这里给出的示例使用在类上:

@Run
class SwordTest()

我们声明的 TestCase注解有个构造函数,传入的参数时一个String类型的ID,把这个注解用在函数上:

  @Run
  class SwordTest() {
        @TestCase(id = "a")
        fun testCase(testId: String) {
            println("Run SwordTest Id = $testId")
        }
   }

上面是注解在代码中的简单使用示例。其中的 @TestCase(id=“a”)是注解构造函数的使用。
注解可以带有参数的构造器。注解参数可支持的数据类型如下:

  • 基本数据类型
  • String
  • KClass
  • enum
  • Annotation
  • 上面的除了基本数据类型为引用的 数组

下面两种声明是不会通过的

annotation class TestCase1(val id: Array<Int>)
annotation class TestCase2(val id: SwordTest)

另外需要注意的是,注解类型中不能有null,因为 JVM不支持将null作为注解属性的值进行存储。
如果注解用作另一个注解的参数时,则其名称不能以 @字符作为前缀。
例如:

annotation class AnnoX(val value: String)
annotation class AnnoY(
       val message: String
       val annoX: AnnoX = AnnoX("X"))

2.3 处理注解
如果没有相应的注解信息处理逻辑流程,那么注解可以说是废了,没有什么实用价值。

首先,我们的目标测试类是:

    @Run
    class SwordTest() {
        @TestCase(id = "a")
        fun testCase(testId: String) {
            println("Run SwordTest Id = $testId")
        }
    }

我们需要在 @TestCase注解作用在函数上的处理过程

  1. ::class 引用
    首先声明一个变量指向SwordTest实例
    然后就可以通过这个变量来获取该对象的类的信息,使用 ::class来获取 sword对象实例的 KClass类的引用
val sword = SwordTest()
val kClass = sword::class

//有点像Java中的 getClass()
//上面这行代码,Kotlin编译器会自动推断出 kClass变量的类型是
val kClass:KClass<out SwordTest> = sword::class
  1. members扩展属性
    下面我们需要获取 sword对象类型所声明的所有函数。Kotlin中 可以直接使用扩展属性 declaredFunctions来获取这个类中声明的所有函数。
//返回的是一个  Collection<KCallable<*>> 其中*是Koltlin泛型中的星投影
val members = kClass.members
  1. annotations属性
    KFunction 类型继承了 KCallable,KCallable又继承了 KAnnotatedElement。KAnnotatedElement中有个public val annotations:List属性里存储了该函数所有的注解信息。通过遍历这个存储Annotation的List,可以获取到TestCase注解:
for (f in members ) {
     f.annotations.forEach {
         if(it is TestCase){
            val id = it.id        //TestCase 注解的属性ID
            doSomething(id)       //注解处理逻辑
         }
     }
}
  1. call函数
    另外,如果想通过反射来调用函数,可以直接使用 call():
f.call(sword, id)
//等价于
f.javaMethod?.invoke(sword, id)

//到这里,我们就完成了一个简单的注解处理器,完整的代码如下:
    @Target(
        AnnotationTarget.CLASS,
        AnnotationTarget.FUNCTION,
        AnnotationTarget.VALUE_PARAMETER,
        AnnotationTarget.EXPRESSION
    )
    @Retention(AnnotationRetention.SOURCE)
    @Repeatable
    @MustBeDocumented
    annotation class TestCase(val id: String)

    class SwordTest {
        @TestCase(id = "a")
        fun testCase(testId: String) {
            println("Run SwordTest Id = $testId")
        }
    }

    fun testAnnoProcessing() {
        val sword = SwordTest()
        val kClass = sword::class

        val members = kClass.members
        for (f in members) {
            f.annotations.forEach {
                if (it is TestCase) {
                    val id = it.id
                    doSomething(id)
                    f.call(sword, id)
                }
            }
        }

    }

    private fun doSomething(id: String) {
        println("Do Something in Annotation Processing $id ${System.currentTimeMillis()}")
    }


//测试:
main(){
  testAnnoProcessing()
}

3. 反射

在Kotlin中我们有两种方式来实现反射功能。
一种是调用Java的反射包下的API
另外一种是 直接调用Kotlin语言提供的 kotlin.reflect包下面的API

不过因为反射功能的应用场景并非所有编程场景都会用到,所以Kotlin把 kotlin.reflect包放到了单独的 kotlin-reflect-1.1.xx.jar下面,也就是说我们如果要使用Kotlin的反射Api,还要去添加依赖。

3.1 类引用
我们先定义一个代码实例:

open class BaseContainer<T>

class Container<T : Comparable<T>> : BaseContainer<Int> {
        val elements: MutableList<T>

        constructor(elements: MutableList<T>) {
            this.elements = elements
        }

        fun sort(): Container<T> {
            elements.sort()
            return this
        }

        override fun toString(): String {
            return "Container(elements = $elements)"
        }
}

反射是在陨石时获取一个类引用。我们已经知道使用 ::class 可以获取到当前对象KClass对象。

val container = Container(mutableListOf<Int>(1, 3, 2, 5, 4, 7, 6))
val kClass = container::class
//如果是要使用Java中的类引用,就要使用javaClass
val jClass = container.javaClass
//或者使用KClass实例的.java属性
val jkClass = kClass.java

3.2 函数引用
例如,有一个简单地判断一个Int整数是否是奇数的函数:

fun isOdd(x: Int) = x % 2 != 0

//代码中调用
>>>isOdd(4)
false

另外,在高阶函数中如果想把它当做一个参数来使用,可以使用 ::操作符

val nums = listOf(1, 2, 3)
val filteredNums = nums.filter(::isOdd)
println(filteredNums)  //输出 [1,3]

这里的 ::isOdd 就是一个函数类型 (Int)->Boolean 值

3.3 属性引用
在Kotlin中,访问属性属于第一级对象,可以使用 "::"操作符

var one = 1

fun testReflectProperty() {
     println(::one.get())
     ::one.set(2)
     println(one)
}

表达式 ::one 等价于类型为KProperty的一个属性,它可以允许我们通过 get()函数获取值。
对于可以边属性 var one = 1,返回类型为 KMutableProperty的值,并且还有 set()方法

3.3 绑定函数和属性引用

  val digitRegex = "\\d+".toRegex()
  digitRegex.matches("a")
  digitRegex.matches("4")
  digitRegex.matches("1")
  digitRegex.matches("O")

我们定义的 digitRegex.matches重复出现,比较显得样板化。
在Kotlin中,可以直接引用 digitRegex对象实例的matches() 方法,上面代码可以这样写:

        val digitRegex = "\\d+".toRegex()
        val isDigit = digitRegex::matches
        isDigit("a")
        isDigit("4")
        isDigit("1")
        isDigit("O")

4. 使用反射获取泛型信息

通过反射可以获取到类的注解、方法、成员变量等,那么能不能通过反射获取到泛型信息呢?
我们知道Java中有泛型擦除,在程序运行时就无法得到泛型。

而当这个类继承一个父类,父类中有泛型信息时,那么就可以通过 调用 getGenericSuperclass()方法得到父类的泛型信息。
getGenericSuperclass()是 Generic继承的特例。对于这种情况子类会保存父类的Generic参数类型。返回一个ParameterizedType。另外,我们所的Java泛型在字节码中会被擦除,并不总是擦除为Object类型,而是擦除到上限类型。

在Kotlin中也是一样的泛型机制,所以通过反射能拿到的 也只能是父类泛型信息的子类泛型

具体的示例代码如下:

    class A<T>

    open class C<T>
    class B<T> : C<Int>()      //继承父类 C<Int>()

    fun fooA() {
        //无法在此处获得运行时T的具体类型 , 下面代码运行时会报错
        val parameterizedType = A<Int>()::class.java.genericSuperclass as ParameterizedType
        val actualTypeArguments = parameterizedType.actualTypeArguments
        for (type in actualTypeArguments) {
            val typeName = type.typeName
            println("typeName = ${typeName}") //运行会报错
        }
    }

    fun fooB() {
        //当继承了 父类C<Int>的时候,在此处能够获得运行时genericSuperclass T的具体类型
        val parameterizedType = B<Int>()::class.java.genericSuperclass as ParameterizedType
        val actualTypeArguments = parameterizedType.actualTypeArguments
        for (type in actualTypeArguments) {
            val typeName = type.typeName
            println("typeName = ${typeName}") //输出 typeName = java.lang.Integer
        }
    }

下面通过一个简单的实例来说明Kotlin中的反射是怎样获取泛型代码的基本信息。
首先声明一个父类BaseContainer:

open class BaseContainer<T>

然后声明一个 Container继承该父类

   class Container<T : Comparable<T>> : BaseContainer<Int> {
        val elements: MutableList<T>

        constructor(elements: MutableList<T>) {
            this.elements = elements
        }

        fun sort(): Container<T> {
            elements.sort()
            return this
        }

        override fun toString(): String {
            return "Container(elements = $elements"
        }
    }

然后我们去获取这个类的信息:

        //在声明一个Container对象实例
        val container = Container(mutableListOf<Int>(1, 3, 2, 5, 4, 7, 6))
        //然后获取container的KClass对象引用
        val kClass = container::class
        //KClass对象的typeParameters属性中存有类型参数的信息,代码示例如下:
        val typeParameters = kClass.typeParameters//获取类型参数typeParameters,也叫泛型信息
        //它是一个数组,我们获取第一个对象
        val kTypeParameters = typeParameters[0]

然后这个 kTypeParameters有如下的信息:
在这里插入图片描述
KClass的constructor属性中 存有构造函数信息,可以从中获取构造函数的入参信息等:

 val constructors = kClass.constructors
        for (KFunction in constructors) {
            KFunction.parameters.forEach {
                val name = it.name
                val type = it.type
                println("name = $name")  //输出 elements
                println("type = $type")  //输出 kotlin.collections.MutableList<T>
                for (KTypeProjection in type.arguments) {
                    println(KTypeProjection.type)   //输出 : T
                }
            }
        }
发布了248 篇原创文章 · 获赞 99 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/rikkatheworld/article/details/103048013