美文网首页kotlin入门潜修
kotlin入门潜修之特性及其原理篇—注解

kotlin入门潜修之特性及其原理篇—注解

作者: 寒潇2018 | 来源:发表于2019-02-14 19:51 被阅读0次

    本文收录于 kotlin入门潜修专题系列,欢迎学习交流。

    创作不易,如有转载,还请备注。

    注解

    注解在编程语言中是一种特殊的机制、是用来描述代码的元数据。在我看来,注解首先简洁了代码,使得我们编码变得言简意赅;其次,注解解耦了系统,同时还让系统之间保持了一定的联系(比如不再像配置文件那样,和代码完全独立);最后,注解能及时有效的反馈错误(编译时报错)。

    看到这里可能还是不太明白什么是注解,如果使用通俗的白话来表示一下注解是什么意思呢?

    我们可以认为,注解就是一种注释,只不过这种注释是给两个角色看的:一个是我们,能够从注解中看出代码的意义来;另一个则是注解处理器(编译器),这个就是注解实际起作用的解释引擎,它会对注解信息做出解释,并执行相应的动作。比如基于注解的编译警告、依赖注入等等。注解处理引擎也是注解机制中最主要的存在。

    如果还不明白,那就接着往下看。

    java中的注解

    java语言提供了很多内置的注解,比如我们常见的下面两种注解:

    public class Test extends Thread {
        @Override
        public void run() {
            super.run();
        }
        @Deprecated
        public void m1() {
        }
    }
    

    没错,我们复写的run方法上面的 @Override就是个注解,表示该方法是超类中的方法,我们在子类进行了复写。 @Deprecated注解则表示该方法不建议被使用了,以后可能会被废弃。

    从代码可以看出来,注解的使用语法是:@+注解名。此外注解还可以接受参数,比如我们常见的SuppressWarnings注解(作用是抑制警告),示例如下:

    public class Test extends Thread {
        @Deprecated
        public void m1() {
        }
    
        @SuppressWarnings("deprecation")
        public static void main(String[] args) {
            Test t = new Test();
            t.m1();
        }
    }
    

    正常来讲,因为m1方法使用了 @Deprecated
    注解修饰,所以我们在main方法调用m1的时候,编译器会警告我们m1方法已经不建议使用,此时,如果我们在m1方法上添加 @SuppressWarnings("deprecation")这个注解,就可以禁止掉这个警告提示。

    当然,这个只是SuppressWarnings注解完成的一个功能,实际上它还可以完成其他许多的功能,这里不再阐述,只需要关注注解是可以接收参数这个特性即可。

    自定义java注解

    前面提到的都是java自带的注解,如果有需要,我们也可以自定义注解,如果不熟悉自定义注解,则可以参考系统提供的注解写法,比如我们先来看下上个章节中的SuppressWarnings这个注解的定义,示例如下:

    @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface SuppressWarnings {
        String[] value();
    }
    

    由上面代码可知,SuppressWarnings注解包含了一个参数,该参数类型是字符串数组,这就是注解入参的写法:我们只需要像定义方法一样定义需要的入参及其类型即可。此外,我们发现,在使用SuppressWarnings的时候,我们并没有指定参数的名称,而是直接传入了“deprecation”这个值,这是为什么?如果有多个参数的时候还能这么传入吗?

    这是java中的一个默认语法,对于只有一个入参的注解,我们可以使用value进行命名,使用value进行命名的参数,在传参的时候可以省略其名称。比如上面的SuppressWarnings("deprecation")就是省略了value的缘故,其全称可以写为:SuppressWarnings(value="deprecation")。

    注意,只能当注解只需要一个参数,且该参数被命名为value的时候,才能省略。

    最后,我们发现在SuppressWarnings注解上面还有一些注解,比如Target注解、Retention注解,这些注解被称为元注解,只能用于修饰注解类型。

    Target注解和Retention注解是我们在定义注解的时候常用的两个元注解。使用Target修饰的注解,用来表明我们自定义注解可以修饰的目标,比如可以用来修饰方法、成员变量、包等等,具体的修饰目标如下所示:

    /** 可以用于修饰类, 接口 (包括注解类型), 或者枚举类 */
        TYPE,
        /** 用于修饰字段 (包括枚举常量) */
        FIELD,
        /** 用于修饰方法 */
        METHOD,
        /** 用于修饰形参 */
        PARAMETER,
        /** 用于修饰构造方法*/
        CONSTRUCTOR,
        /** 用于修饰本地变量 */
        LOCAL_VARIABLE,
        /**  用于修饰注解*/
        ANNOTATION_TYPE,
        /** 用于修饰包 */
        PACKAGE,
        /**
         * 用于修饰类型参数,从jdk1.8开始支持
         */
        TYPE_PARAMETER,
        /**
         * 可用于修饰各种类型
         * @since 1.8
         */
        TYPE_USE
    

    而Retention注解则表示我们自定义注解的生效时机,它总共支持三个值,如下所示:

        /**
         * 只在源代码中生效,编译的时候会被擦除
         */
        SOURCE,
    
        /**
         * 注解信息会被编译器编译到class文件中,但无法再运行时保持,这意味着我们无法再运行时采用反射来获取到这个注解,这个是所有注解的默认行为
         */
        CLASS,
    
        /**
         * 注解信息会被编译到class文件中,而且在运行时保持了该注解信息,可以使用反射机制获取到该注解信息
         */
        RUNTIME
    

    结合上面的信息,我们演示下自定义注解的写法,如下所示:

    public @interface MyAnnotation {
        String firstParam();
        int secondParam();
    }
    

    上面代码我们定义了一个接收两个入参的注解,其语法为权限修饰符 + @interface + 注解名。该注解信息同时会在编译期和运行期生效(参见上面阐述过的默认取值)。

    乍看起来,注解的写法有点像接口的写法,形式上确实是这样,但需要注意以下几点:

    1. 同接口一样,注解只能使用包权限修饰符或者public权限修饰符进行修饰。
    2. 和接口不同的是,注解的关键字是@interface,并不是interface

    那么,如何使用我们自定义的注解呢?这个很简单,其使用姿势与使用系统提供的注解姿势一致,如下所示:

    @Retention(RetentionPolicy.RUNTIME)
    public @MyAnnotation(firstParam = "param1", secondParam = 1)
    public class Test {}
    

    使用注解的姿势就是@+注解名,然后附带上注解需要的参数即可。

    kotlin中的注解

    了解完java中的注解后,再来看下kotlin中的注解。

    首先,在kotlin中,同样为我们提供了几个常见的内置注解,这些注解位于kotlin.annotation包下,它们只能用于修饰注解类型,也就是元注解。介绍如下:

    //注解的修饰目标,与java类似,但是有很多不同
    @Target
    //同java基本一致,定义有稍微不同
    @Retention
    //新注解,表示一个注解可以在同一个地方写多遍,
    //在1.8版本之前的jvm,只支持Retention为Source的写法
    @Repeatable
    //用该注解修饰的注解,将会被视为api方法的一部分,
    //在生成文档的时候将会保留注解相关信息
    @MustBeDocumented
    

    下面简单介绍下上面的几个元注解。

    首先,来看下Target修饰的注解可以用于修饰哪些目标,如下所示:

    /**用于修饰类,、接口、对象类(object)、注解类*/
        CLASS,
        /** 只能用于修饰注解类,实际上Target注解本身就是使用该类型修饰的,所以Target只能用于注解 */
        ANNOTATION_CLASS,
        /** 用于修饰泛型类型参数 (直接写本篇文章时,暂未支持) */
        TYPE_PARAMETER,
        /** 用于修饰属性*/
        PROPERTY,
        /** 用于修饰字段, 包括属性的后备字段 */
        FIELD,
        /** 用于修饰本地变量 */
        LOCAL_VARIABLE,
        /** 方法或者构造方法中的值参数 */
        VALUE_PARAMETER,
        /** 只能用于修饰构造方法(第一或者第二)*/
        CONSTRUCTOR,
        /** 用于修饰方法,不包括上面的构造方法 */
        FUNCTION,
        /**只能用于修饰getter属性 */
        PROPERTY_GETTER,
        /**只能用于修饰setter属性 */
        PROPERTY_SETTER,
        /**同java,用于修饰类型 */
        TYPE,
        /**用于修饰任意的表达式*/
        EXPRESSION,
        /** 用于修饰文件*/
        FILE,
        /** 用于修饰别名类型,从kotlin1.1开始支持 */
        TYPEALIAS
    

    然后来看下Retention修饰的注解可以修饰哪些目标,如下所示:

        /** 源代码层次 */
        SOURCE,
        /** 存在于编译期,运行时不可见 */
        BINARY,
        /** 即存在于编译期,又能在运行时访问到,默认即是该值*/
        RUNTIME
    

    而对于Repeatable和MustBeDocumented注解都没有参数,因此只有单一的意义,此处略过。

    那么kotlin中的注解写法和java中的注解有什么不同吗?这里通过代码展示下kotlin中注解的使用方式,注意代码中的注释:

    //这里定义了一个注解,可被文档化,能修饰类、属性、
    //构造方法、本地变量,除此之外不能修饰其他目标
    //同时该注解的信息既在编译器保留,同时在运行时也能获取到
    @MustBeDocumented
    @Target(AnnotationTarget.CLASS, AnnotationTarget.CONSTRUCTOR, AnnotationTarget.PROPERTY, AnnotationTarget.LOCAL_VARIABLE)
    @Retention(AnnotationRetention.RUNTIME)
    annotation class TestAnnotation(val value: String)
    
    @TestAnnotation("class")//正确,可以修饰类
    class Test {
        @TestAnnotation("property"))//正确,可以修饰属性
        private val t1 = "test"
    
        @TestAnnotation("constructor"))//正确,可以修饰构造方法
        constructor()
    
        @TestAnnotation("method"))//!!!错误,不能修饰方法
        fun m1() {
            @TestAnnotation("local_variable"))//正确,可以修饰本地变量
            val i = 1;
        }
    }
    

    可见kotlin中的注解与java中的注解大同小异,但需要注意的是kotlin自定义注解的语法,不再是像java一样使用@interface来进行定义,而是使用annotation class这种语法。

    kotlin精确注解

    “精确注解”产生的原因,主要是kotlin注解与java层面上注解的不同而引起的,先来看个例子:

    class Test( val param1: Int)
    

    上面是一个极其简单的kotlin类声明,主构造方法接收一个Int类型的参数param1,那么这段代码生成的字节码是什么呢?来看下:

    public final class Test {
      // access flags 0x12
      private final I param1
      // access flags 0x11
      public final getParam1()I
       //...省略一些字节码
      // access flags 0x1
      public <init>(I)V
       //...省略一些字节码
    

    由字节码可知,对于上面的代码,kotlin为我们生成了构造方法,并为param1生成了一个属性成员以及一个公有的get方法,那么问题来了,如果我们在param1上加上注解,该注解到底会在哪儿生效呢?比如,下面一段代码:

    //我们定义了一个注解TestAnnotation
    annotation class TestAnnotation
    //使用注解TestAnnotation修饰param1,此时注解作用的目标是什么?
    class Test(@TestAnnotation val param1: Int)
    

    上面代码中,注解TestAnnotation到底作用于param1对应的属性成员还是其对应的get方法呢?很简单,来看下字节码,如下所示:

    //...省略部分字节码
      public <init>(I)V
        @LTestAnnotation;()
    //...省略部分字节码
    

    通过查看字节码,我们发现,TestAnnotation被编译到了构造方法中,即默认修饰的是构造方法中的参数,而不是属性和get方法!那么如何明确指定TestAnnotation注解在java层面上的修饰目标呢?这就引入了“精确注解”的概念,所谓“精确注解”是指,kotlin允许我们精确指定注解在java层面上的修饰目标。

    比如,同样是上面的代码,我们想让注解实际作用域java层面上的get方法,我们就可以这么写:

    class Test(@get:TestAnnotation val param1: Int)
    

    其对应的字节码如下所示:

    //...省略部分字节码
      public final getParam1()I
      @LTestAnnotation;()
    //...省略部分字节码
    

    通过上面字节码我们发现,实际上TestAnnotation注解已经最用于param1对应的get方法上了!这就是“精确注解”。

    kotlin为我们提供了多个“精确注解”,当我们使用这些注解的时候,其相应的作用罗列如下:
    —file:作用于文件
    —property :用该值修饰的注解对java来说是不可见的
    —field: 字段
    —get : 用于修饰属性的getter
    —set :用于修饰属性的setter
    —receiver :用于修饰扩展属性以及方法的receiver
    —param :用于修饰构造参数
    —setparam :用于修饰属性的setter参数
    —delegate :用于修饰委托属性
    如果没有指定“精确注解”,那么kotlin编译器会根据注解上的target取值来决定修饰哪个目标,如果有多个目标,kotlin规定了他们的优先级:第一优先级是param;然后是property;最后是field。

    注解背后的机制

    本小节来结合Retention看下kotlin注解背后的原理,首先看下要分析的代码,如下所示:

    @Target(AnnotationTarget.CLASS)
    annotation class TestAnnotation(val value: String)
    //修饰类Test
    @TestAnnotation("class")
    class Test {
    }
    

    其生成的字节码如下所示:

    public abstract @interface TestAnnotation implements java/lang/annotation/Annotation  {
      @Lkotlin/annotation/Target;(allowedTargets={Lkotlin/annotation/AnnotationTarget;.CLASS})
    
      @Ljava/lang/annotation/Retention;(value=Ljava/lang/annotation/RetentionPolicy;.RUNTIME)
    
      @Ljava/lang/annotation/Target;(value={Ljava/lang/annotation/ElementType;.TYPE})
    
      // access flags 0x401
      public abstract value()Ljava/lang/String;
    }
    

    由上面字节码可以总结如下:

    1. kotlin注解实际上会被编译成实现java.lang.annotation.Annotation接口的一个抽象类。这个与java相一致,因为在java中只有实现了Annotation接口的才被当做注解对待(而且还不能是自己实现,必须是系统实现)!
    2. 注解的入参,实际上会被编译成抽象的公有方法。这些公有的抽象方法,就是用于获取注解参数的对外方法!比如我们想获取上述TestAnnotation注解的vlaue值,如下所示:
        Test::class.annotations.forEach {
            val anotation:TestAnnotation = it as TestAnnotation
            println(anotation.value)//打印 'class'
        }
    

    其对应的字节码摘录如下所示:

       L8
        LINENUMBER 24 L8
        ALOAD 5
        INVOKEINTERFACE TestAnnotation.value ()Ljava/lang/String;//调用了value方法!!!
        ASTORE 6
       L9
    
    1. 如果我们不加任何元注解信息,则编译器会为我们加上java层面上的默认元注解(如默认的@Retention(RetentionPolicy.RUNTIME)
      ),但当我们显示使用kotlin语言指定Retention策略的时候,编译器则会同时提供kotlin层面的注解,如下所示:
    //自定义注解信息
    @Retention(AnnotationRetention.RUNTIME)
    annotation class TestAnnotation(val value: String)
    //生成的字节码
    public abstract @interface TestAnnotation implements java/lang/annotation/Annotation  {
    //kotlin层面的注解信息
      @Lkotlin/annotation/Retention;(value=Lkotlin/annotation/AnnotationRetention;.RUNTIME)
    //java层面的注解信息
      @Ljava/lang/annotation/Retention;(value=Ljava/lang/annotation/RetentionPolicy;.RUNTIME)
      // access flags 0x401
      public abstract value()Ljava/lang/String;
    }
    
    1. 关于Retention注解,可以通过字节码来查看其取不同取值时的区别,比如我们定义如下代码:
    @Retention(AnnotationRetention.CLASS)
    annotation class TestAnnotation(val value: String)
    //使用注解
    @TestAnnotation("class")
    class Test { }
    //对应的字节码将会包含该注解信息,如下所示:
    public final class Test {
      @LTestAnnotation;(value="class")
    }
    

    下面我们改变Retention的策略,如下所示:

    //将Retention的策略改成SOURCE
    @Retention(AnnotationRetention.SOURCE)
    annotation class TestAnnotation(val value: String)
    

    那么,此时我们将不能在字节码中看到注解信息!关于RUNTIME机制这里无法从字节码中进一步验证,只能通过代码来演示在运行时获取注解信息,这个会在下节案例实战中进行阐述。

    注解案例实战

    前面巴拉巴拉一大堆注解的语法,看来看去怎么感觉注解没有啥用?如果单纯像上面那样自己定义注解,然后拿着自定义的注解去修饰下目标,确实没有什么用!那么注解存在的意义是什么?除了几个系统自带的注解外,我们自定义的注解又该如何工作?

    这些问题正是我们本小节要阐述的问题,现在我们来写一个demo,演示下实战中注解的应用。

    这个demo的目的就是完成依赖注入功能!没错,就是很多框架提供的功能,这里我们提供一个非常“简版”的kotlin实现。

    首先,先来了解下什么是依赖注入。在写代码的时候,如果在类A中使用到类B的对象的时候,我们一般会直接在类A中进行new B()操作,这样就满足了我们的需求。这么做有什么弊端呢?

    这种写法的弊端就是,A类需要对B对象的创建以及其生命周期负责,这样显然会带来系统的高耦合性。举个简单的例子,试想一下,如果此时B的构造方法变了,不再是无参的构造方法,那么是不是也需要去变更A类中的代码?如果有多个类依赖了B,是不是要逐个修改这些类中的代码?答案是显然的,这种牵一发而动全身的系统绝对不是我们想要的,因此有一种解耦的设计模式(思想)就出现了,它就是控制反转。

    控制反转要解决的问题就是,不再将B的创建放到A类中,而是交给A类的使用者,比如我们可以为A类提供一个构造方法,该构造方法接收B类型的对象,这样我们就可以在A中使用由外界传入的B对象。再进一步考虑,为了减免外部构造的复杂性(即使用的时候,我们无需关注B对象的构建,也无需关注A类是不是需要B对象),我们可以提供一个“中间者”来完成B对象的构造、赋值功能,这样就大大解耦了系统,这个“中间者”就是我们demo要实现的功能,也是各大框架中所谓的“容器”。

    控制反转已经理解了,那么上面提到的依赖注入是什么?其实上面的描述的具体操作就是依赖注入,控制反转是一种设计模式、一种思想,而依赖注入是这种模式的一种具体实现。依赖注入使得获取依赖对象的方式反转了。

    白话一大堆,说的自己都觉得玄乎其玄,其实最好的例子就是看代码!想起一句话,代码面前了无秘密!

    demo的场景是这样的,我们有一个画笔类Paint,以及一个颜色类Color,我们将用这两个类完成绘制功能。很显然,按照常规方案,我们只需要在画笔内部生成一个Color对象,然后完成绘制即可,示意如下:

    //color类,用于获取画笔的颜色
    class Color {
        fun getColor(): String {
            return "red"
        }
    }
    //画笔
    class Paint {
    //这里我们直接在画笔类中生成了Color对象
        private val color = Color();
    //完成绘制
        fun draw() {
            println("draw with color: " +color.getColor())
        }
    }
    //测试代码
    fun main(args: Array<String>) {
        val paint = Paint()
        paint.draw()//打印 draw with color: red
    }
    

    上面就是我们常规的实现方案,但是根据上文分析可知,这种实现方式显然具有很强的耦合性,因此我们需要采用“控制反转”的思想来完成对Paint类中的color字段的赋值。

    首先,我们定义一个注解,该注解位于test包下,如下所示:

    package test
    annotation class Inject //注解Inject,位于test包中
    

    然后,我们抽象出来一个“中间者”,这个“中间者”的作用就是解析Inject注解、完成控制反转,如下所示:

    //中间者Ioc类
    class Ioc {
    //定义了一个伴随对象,方便外部调用
        companion object {
    //inject方法接收两个参数,一个是宿主对象,一个是宿主对象的类类型
    //这个宿主对象是相对于注入对象来讲的,比如在本例中,Paint类型的对象
    //就是宿主对象,而Color类型对象就是被注入的对象
            fun inject(obj: Any, clazz: KClass<out Any>) {
                for (memberProperty in clazz.memberProperties) {//遍历成员变量
                    memberProperty.annotations.forEach {//读取成员变量上面的注解
                        if (it.annotationClass.qualifiedName.equals("test.Inject")) {//如果该成员变量使用了“Inject”注解修饰
                            memberProperty.isAccessible = true//修改访问权限为"public"
                            memberProperty.javaField?.set(obj, memberProperty.javaField?.type?.newInstance())//为该字段生成实例,并重新赋值
                        }
                    }
                }
            }
        }
    }
    

    上面就是我们的中间者实现代码,代码中的注释已经比较详细,这里不再展开。下面看下如何使用:

    //Color类,保持不变
    class Color {
        fun getColor(): String {
            return "red"
        }
    }
    //画笔类
    class Paint {
    //注意这里!我们不再直接生成Color对象,而是使用Inject注解
    //进行了修饰,意思是交给我们的"中间者"来将其实例化
    //由于无法立即赋值,所以需要使用lateinit关键字修饰,表明该字段会
    //在合适的时候进行实例化
        @Inject
        private lateinit var color: Color
      
        fun draw() {
            println("draw with color: " +color.getColor())
        }
    }
    //测试代码
    fun main(args: Array<String>) {
        val paint = Paint()
        Ioc.inject(paint, paint::class)//这里我们将实例化的控制权交给了Ioc
        paint.draw()//打印'draw with color: red'
    }
    

    至此,本篇文章已经阐述完毕。

    相关文章

      网友评论

        本文标题:kotlin入门潜修之特性及其原理篇—注解

        本文链接:https://www.haomeiwen.com/subject/xzrqeqtx.html