美文网首页
从字节码角度了解Java注解

从字节码角度了解Java注解

作者: JeckZyang | 来源:发表于2020-04-29 17:56 被阅读0次

    注解定义

    Java注解(Annotaion)又称Java标注,是JDK5.0引入的一种注释机制.注解也是元数据的一种形式,提供有关于程序,但不属于程序本身的数据. 注解对他们注解的代码的操作没有直接影响. 本文后面会从字节码文件中分析注解.

    注解作用

    注解本身没有任何意义,单独的注解就是一个注释, 它需要结合APT,反射,插桩技术才有意义. 在后续的文章中,会陆续给大家介绍这些技术.

    元注解

    在引入注解的架构之前,先引入什么是元注解.
    作用在注解上面的注解叫元注解,其中包括@Retention @Target @Document @Inherited四种. 本文后面会介绍我们自己如何自定义一个元注解.

    @Document:用于描述其它类型的annotation应该被作为被标注的程序成员的公共API, 因此可以被例如javadoc此类的工具文档化.它是一个标记注解,没有成员。

    @Inherited: 使用此注解声明出来的自定义注解.在使用此自定义注解时,
    如果注解在类上面时,子类会自动继承此注解,否则的话,子类不会继承此注解.这里一定要记住,使用Inherited声明出来的注解,只有在类上使用时才会有效,对方法,属性等其他无效.

    @Target:用于描述注解的范围,即注解在哪用。它说明了Annotation所修饰的对象范围:
    Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)
    、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量等。
    取值类型(ElementType)有以下几种:
    CONSTRUCTOR:用于描述构造器
      FIELD:用于描述域即类成员变量
      LOCAL_VARIABLE:用于描述局部变量
      METHOD:用于描述方法
      PACKAGE:用于描述包
      PARAMETER:用于描述参数
      TYPE:用于描述类、接口(包括注解类型) 或enum声明
      TYPE_PARAMETER:1.8版本开始,描述类、接口或enum参数的声明
      TYPE_USE:1.8版本开始,描述一种类、接口或enum的使用声明

    @Retention用于描述注解的生命周期,表示需要在什么级别保存该注解,即保留的时间长短。
    取值类型(RetentionPolicy有以下几种:
    1,SOURCE: 标记的注解仅保留在源码级别中,并被编译器忽略
    2,CLASS: 标记的注解在编译时由编译器保留,但Java虚拟机(JVM)会忽略.
    3,RUNTIME:在运行时有效(即运行时保留)
    其实可以说是 \color{red}{RUNTIME > CLASS > SOURCE}
    SOURCE可以用CLASS 和 RUNTIME 代替,
    CLASS 可以用RUNTIME代替,反之不可.

    Annotation 架构

    CB4599CEC57E8B9FBCD123A8852CD4BA.png
    • Annotation 的每一个实现类(\color{red}{包括自定义注解}),都和 1 个 RetentionPolicy 关联 并且和1~n个ElementType 关联"。
    • Annotation 有许多系统已经帮我们实现的类,包括:Deprecated, Documented, Inherited, Override 等等。

    下面的代码是自定义一个普通注解,并且有详细的注释.

    /*用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,
        因此可以被例如javadoc此类的工具文档化。它是一个标记注解,没有成员。
    */
    //@Documented
    /*
        Inherited作用是,使用此注解声明出来的自定义注解,在使用此自定义注解时,
        如果注解在类上面时,子类会自动继承此注解,否则的话,子类不会继承此注解。
        这里一定要记住,使用Inherited声明出来的注解,只有在类上使用时才会有效,
        对方法,属性等其他无效。
     */
    //@Inherited
    /*
       用于描述注解的范围,即注解在哪用。它说明了Annotation所修饰的对象范围:
       Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)
       、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量等。
        取值类型(ElementType)有以下几种:
       CONSTRUCTOR:用于描述构造器
      FIELD:用于描述域即类成员变量
      LOCAL_VARIABLE:用于描述局部变量
      METHOD:用于描述方法
      PACKAGE:用于描述包
      PARAMETER:用于描述参数
      TYPE:用于描述类、接口(包括注解类型) 或enum声明
      TYPE_PARAMETER:1.8版本开始,描述类、接口或enum参数的声明
      TYPE_USE:1.8版本开始,描述一种类、接口或enum的使用声明
     */
    @Target({ElementType.METHOD,ElementType.TYPE})// 1~n个ElementType
    /*
     用于描述注解的生命周期,表示需要在什么级别保存该注解,即保留的时间长短。
        取值类型(RetentionPolicy)有以下几种:
        SOURCE:在源文件中有效(即源文件保留)
        CLASS:在class文件中有效(即class保留)
     RUNTIME:在运行时有效(即运行时保留)
    
       其实可以说是 RUNTIME > CLASS > SOURCE
       SOURCE可以用CLASS 和 RUNTIME 代替,
       CLASS 可以用RUNTIME代替,反之不可.
     */
    @Retention(RetentionPolicy.SOURCE) //一个RetentionPolicy
    public @interface ArouterAnnotaion {
        String value() default "Zyang真帅";
    }
    

    从上图可以看出,java中所有的注解都默认实现了Annotation接口, 有人会说我们自定义的注释没看到有implements实现啊.本文后面会为你揭晓.

    这是Annotation.java的源码,从源码中可以看出,Anntation是一个接口. 604849DA996CD134418BA5B98DED55F6.png

    下面写一个自定义的注解,并没有看到实现了Annotation的接口啊

    public @interface ArouterAnnotaion {
    }
    

    其实在java在编译的时候, 编译器默认的给我们加上了implements java/lang/annotation/Annotation
    下图是ArouterAnnotation字节码文件. 可以通过javap命令查看字节码,也可以通过ASM工具.


    image.png
    image.png

    下面是对注解具体介绍:

    一, 介绍RetentionPolicy
    • RetentionPolicy.SOURCE,标记的注解仅保留在源码级别中,并被编译器忽略
    File: AAnnotaion.java
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.SOURCE)
    public @interface AAnnotaion {
         String value() default "Zyang真帅";
         int id();
    }
    
    File: MyClass.java
    @AAnnotaion(value = "value",id = 1)
    public class MyClass {
    
    }
    
    通过字节码查看, 字节码里面并没有任何注解的信息, 说明RetentionPolicy.SOURCE只作用在源码级别中. image.png
    • RetentionPolicy.CLASS标记的注解在编译时由编译器保留,但Java虚拟机(JVM)会忽略.
    File:AAnnotaion.java
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.CLASS)
    public @interface AAnnotaion {
        String value() default "Zyang真帅";
        int id();
    }
    
    File:MyClass.java
    @AAnnotaion(value = "value",id = 1)
    public class MyClass {
    
    }
    
    通过字节码查看, 字节码里面是有注解的信息的.并且这条信息被标识上了 invisible,告知JVM虚拟机忽略这条信息为不可见的. image.png
    • RetentionPolicy.RUNTIME 标记的注解由JVM保留,因此运行时环境可以使用它.
    File:AAnnotaion.java
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AAnnotaion {
        String value() default "Zyang真帅";
        int id();
    }
    
    File:MyClass.java
    @AAnnotaion(value = "value",id = 1)
    public class MyClass {
       
    }
    
    通过字节码查看, 字节码里面是有注解的信息的. image.png
    二, 介绍Target

    主要是用于描述注解的范围,即注解用在在哪里. 由于很简单,文章前面已经有介绍了.这里不作介绍了.

    其他两个元注解, 使用特别少,文章前面有简单介绍,这里不作介绍了.

    注解的使用

    在自定义注解中, 如果元素有default, 在使用这个自定义注解,可以不用去赋值,他会使用这个默认值; 如果没有default, 在使用时,必须要给这个注解的元素赋值. 赋值的方式以key-value形式赋值

    File:AAnnotaion.java
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AAnnotaion {
        String value() default "";
        int id();
    }
    

    它的使用

    File:MyClass.java
    @AAnnotaion(id = 1)
    public class MyClass {
        
    }
    
    File:MyClass1.java
    @AAnnotaion(id = 1,value="value")
    public class MyClass1 {
        
    }
    

    注解的应用场景

    根据注解的保留级别不同, 对注解的使用自然存在不同的场景. 由注解的三个不同保留级别可知,注解作用于:源码(RetentionPolicy.SOURCE), 字节码(RetentionPolicy.CLASS),运行时(RetentionPolicy.RUNTIME).

    源码级别(RetentionPolicy.SOURCE)

    一般使用在 Apt技术,IDE语法检查

    • APT技术:在编译时期能够获取注解与注解生命的类,包括类中所有的成员信息, 一般用于生成额外的辅助类. 例如(ButterKnife,Arouter).

    • IDE语法检查: 在Android开发中, support-annotations与androidx.annotaion中均有提供@IntDef注解,这注解主要是提供语法检查的, 此注解源码如下

    @Retention(SOURCE)
    @Target({ANNOTATION_TYPE})//这是作用在注解上面的注解, 也是元注解
    public @interface IntDef {
        /** Defines the allowed constants for this element */
        int[] value() default {};
    
        /** Defines whether the constants can be used as a flag, or just as an enum (the default) */
        boolean flag() default false;
    
        /**
         * Whether any other values are allowed. Normally this is
         * not the case, but this allows you to specify a set of
         * expected constants, which helps code completion in the IDE
         * and documentation generation and so on, but without
         * flagging compilation warnings if other values are specified.
         */
        boolean open() default false;
    }
    

    在Java中Enum(枚举)的实质是特殊单例的静态成员变量,在运行期所有枚举类作为单例,全部加载到内存中.比常量多5到10的内存占用. 这时候此注解的意义在于能够取代枚举.

    public enum Teacher{
      ZHANGSAN,HANGMEIMEI
    }
    
    public void test(Teacher teacher){
    
    }
    
    test(Teacher.ZHANGSAN)
    

    为了内存优化,我们现在不再使用枚举,这时候我们使用常量来代替枚举.

    public static final int ZHANGSAN = 0;
    public static final int HANGMEIMEI = 1;
    

    但是我们使用的时候, 不能限制开发人员固定的某些值.

    test(11);
    test(0);
    

    我们有更加好的代替方案,这时候我们使用@IntDef

    @IntDef({ZHANGSAN,HANGMEIMEI})
    @Target({ElementType.FIELD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Teacher{
    }
    
    public void test(@Teacher int curTeacher){
    
    }
    
    /*
      这样就能达到使用枚举的效果了,如果你使用其他的, 
      IDE就会提示语法错误, 让我们必须使用上面两个值.  
      主要是由IDE 去实现的语法监测.. 
    */
    test(ZHANGSAN);
    
    字节码级别(RetentionPolicy.CLASS)

    一般使用在字节码增强(字节码插桩技术,热修复等....).
    在编译出Class后,通过修改Class数据以实现修改代码逻辑目的,对于是否需要修改的区分或者修改不同逻辑的骗到可以使用注解. 这里不作介绍,后续会发表插桩,热修复等技术文章.

    运行时级别(RetentionPolicy.RUNTIME)

    一般是在反射技术
    在程序运行期间, 通过反射即使动态获取注解与其元素,从而完成不同的逻辑判断处理. 后续会发表反射技术.

    相关文章

      网友评论

          本文标题:从字节码角度了解Java注解

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