• Java 注解


    什么是注解

    注解(Annotations)是 Java5 开始提供的功能特性,注解的定义和接口有些相似,最直观感觉就是比接口时多一个 @ 符号。

    public @interface MyAnnotation {
        String value();
    }
    

    通过 "@ + 注解名" 来使用注解。

    @MyAnnotation(value = "hahaha")
    public class AnnotationUse {
        // ...
    }
    

    那么注解有什么用呢? 注解是用来为类、接口、方法和字段等提供元数据信息,这些信息可以被编译器,开发工具和其他程序等识别,能够在编译和运行时访问元数据信息,然后程序根据这些信息来做一些事情。

    例如,编译器根据注解来提示错误和警告信息,文档工具可以通过读取注解生成文档,程序运行时读取元数据(配置信息)来执行某些操作等。

    换个方式说,注解就是贴在类上的标签。

    打个比方,比如我们去超市买面包,如果把自己看做是运行的程序或编译器,把面包看做 Java 类,面包上的标签就相当于注解,标签上有生产日期,保质期,通过这些标签直接从面包上获得了一些信息,通过读取这些信息我们可以做一些选择或行为,买或者不买,或买几个。注解可以这样理解,就是标签。

    商品上有多个标签,不同用途。Java 注解也一样,也可以标注多个注解。

    定义注解

    通过 @interface 来定义注解,注解里包含方法声明,方法可称为注解元素或属性。

    public @interface MyAnnotation {
       int id();
       String value();
       // ... 
    }
    

    标记注解

    标记注解就是没有任何元素的注解类型,称之为标记注解。

    public @interface MyAnnotation {
    }
    

    单元素注解

    具有一个方法元素的注解类型称为单元素注解,单元素注解中唯一元素的名称应该为 value(也可以不为 value, 但 value 在使用时有特殊的约定支持)。

    public @interface MyAnnotation {
    	String value();
    }
    

    单元素注解元素名为 value 时,使用注解时可以省略指定属性名称。

    @MyAnnotation("hahaha")
    public class AnnotationUse {
    	// ...
    }
    

    如果不为 value 时,比如为 valuexxx,这时必须指定元素名称。

    @MyAnnotation(valuexxx = "hahaha")
    public class AnnotationUse {
    	// ...
    }
    

    注解的元素

    注解中定义的元素(方法)不能带参数或抛出(throws)异常,也不能是默认的方法(接口 default 方法)

    public @interface MyAnnotation {
        //int id() throws Exception; // 错误的
        //String value(String val);  // 错误的,不能带参数
        //default String description() { // 错误的,不能是默认方法
        //  return "return value";
        //}
    }
    

    默认值

    注解元素可以通过 default="default value xxx" 来设置默认值,如果指定的默认值类型与元素的类型不一致会导致编译报错。

    有默认值的注解元素使用时不设置值时会直接使用默认值,如果是没有默认值的注解元素,使用时必须指定值。

    public @interface MyAnnotation {
        // int id() default "666"; // 错误的,默认值与元素类型不一致
        int id();
        String value() default "default value";
    }
    
    // **********************
    
    // value 会使用默认值
    @MyAnnotation(id = 666)
    public class AnnotationUse {
    }
    
    // 编译报错,id 没有默认值,必须定义属性 id
    @MyAnnotation(value = "hahaha")
    public class AnnotationUse {
    }
    

    默认值不会被编译到类上的注解中,而是在使用时动态获取的。所以,修改注解元素的默认值,不管使用注解的类没有重新编译,其获得的默认值均已改变。

    注解的元素类型

    注解中的元素的声明返回类型是有限制的,必须是下列类型之一:

    • 八种基本类型
    • String
    • Class
    • enum
    • annotation
    • 以上类型的一维数组

    例如:

    public @interface MyAnnotation {
        Class<?> clazz(); 
        Class<? extends List> clazz2(); 
    }
    
    
    // clazz2 元素值受泛型有界通配符约束,只能是 List 的子类
    @MyAnnotation(clazz = Object.class, clazz2 = ArrayList.class)
    public class AnnotationUse {
        // ...
    }
    
    public @interface MyAnnotation {
        String[] arrValue(); 
    }
    
    // arrValue 属性是数组,通过 “{}” 来使用,多个值用 ‘,’ 隔开
    @MyAnnotation(arrValue = { "value1", "value2" })
    public class AnnotationUse {
        // ...
    }
    

    如果元素类型不是约定的那几种类型,则会编译错误。

    不能使用的元素

    每个注解都默认继承 java.lang.annotation.Annotation 接口,自定义的注解不能覆盖Annotation 接口中的方法,否则会编译错误。

    Annotation 接口包含如下方法:

    Annotation接口

    public @interface MyAnnotation {
        int hashCode();   // 错误的,不能覆盖 Annotation 接口中的方法
        String toString();// 错误的
    }
    

    注解循环引用

    注解类型元素不能直接或间接地包含本身类型(不能循环引用),否则编译错误。

    public @interface MyAnnotation2 {
    	MyAnnotation myannotation();
    }
    
    public @interface MyAnnotation {
        MyAnnotation myannotation();   // 不能使用自身作为元素类型
        MyAnnotation2 myannotation2(); // 也不能间接使用自身作为元素类型
    }
    

    元注解

    Java 为我们提供了多个元注解(标记其他注解的注解)。如 @Target 用来约束和限定注解在哪些位置使用(如在类、方法、局域变量上使用等),使用 @Repeatable 来设置在某个位置重复使用,@Inherited 用来标注注解能被其他注解继承,通过 @Retention 指定注解的生命周期等。

    元注解详情见下方"Java 的内置注解":

    Java内置注解

    @Override
    用于指定重写超类型中的方法声明。如果加上这个注解的方法不是重写方法,编译器会报告一个错误。

    @SuppressWarnings
    用来抑制编译器警告。

    @Deprecated
    用于标注已被弃用的程序元素(如类,接口,构造方法, 方法等等),当使用不推荐的程序元素时,编译器会发出警告,不建议使用该程序元素。

    @SafeVarargs
    1.7 版本
    用于方法或构造函数,抑制关于不可具体化变量(vararg)类型的未检查警告。

    @FunctionalInterface
    1.8 版本
    用于指定接口为函数接口,函数接口只能有一个抽象方法,如果不符合函数式接口定义编译器将报错。函数接口可以使用 lambda 表达式。

    @Target
    用来约束注解的使用范围。

    @Retention
    用来指定注解的生命周期,分别为 SOURCE, CLASS(没该注解时默认为CLASS), RUNTIME。

    @Inherited
    用于指定注解是否可以被继承,即允许子类继承父类的注解。

    @Repeatable
    1.8 版本
    指定注解可以重复使用 。

    @Documented
    它的作用是将被修饰的注解生成到 javadoc 中去。

    其中 @Retention@Target@Inherited@Repeatable@Documented 等是元注解,在定义注解时会用到他们,下面详细了解一下元注解:

    @Retention

    用来指定注解的生命周期。使注解保留到源码、字节码还是运行时。

    @Retention 注解有一个 RetentionPolicy 类型的 value 属性。

    // ...
    public @interface Retention {
        RetentionPolicy value();
    }
    

    RetentionPolicy 为一个枚举类型。

    public enum RetentionPolicy {
        /**
         * 注解只会在源码中,编译时类上的注解会被编译器丢弃。
         */
        SOURCE,
        /**
         * 编译器把注解记录在 class 文件中,但是在运行时不需要由 VM 保留。
         * 如不指定 @Retention 时,值默认为 CLASS。
         */
        CLASS,
        /**
         * 编译器把注解记录在 class 文件中,并在运行时 VM 会保留,
         * 因此可以通过 Java 反射来读取注解信息。
         */
        RUNTIME
    }
    

    所以三种生命周期长短顺序为 SOURCE < CLASS < RUNTIME 。

    完整代码示例请前往 Github 查看: https://github.com/newobjectcc/code-example/tree/master/basic/annotation/AnnotaionDemo/src/retention

    @Target

    该元注解用来约束注解使用范围。@Target 注解只有一个 ElementType[] 类型的 value 属性。

    // ...
    public @interface Target {
        ElementType[] value();
    }
    

    @Target 注解相同的枚举值在 value 属性上只能出现一次,否则会编译时报错。

    如果声明的注解上没有 @Target,那么适用于除类型参数声明外的所有情况下。

    其中 ElementType 是一个枚举类型:

    public enum ElementType {
        // 用于类, 接口 ,注解, 枚举类型
        TYPE, 
        // 用于域,包括枚举常量
        FIELD, 
        // 用于方法,包括注解类型的元素
        METHOD,
        // 用于形参声明
         PARAMETER, 
        // 可用于构造函数
        CONSTRUCTOR,
        // 用于局部变量声明,包括for语句的循环变量和try-with-resources语句的资源变量
        LOCAL_VARIABLE,
        // 用于其他注解上
        ANNOTATION_TYPE,
        // 用于包声明
        PACKAGE, 
        // 用于泛型类、接口、方法和构造函数的type参数声明,从JDK8开始
        TYPE_PARAMETER,
        /** 标注使用类型,从JDK8开始
         * https://docs.oracle.com/javase/specs/jls/se12/html/jls-4.html#jls-4.11
         */
        TYPE_USE,
        // 用于模块, 从JDK9开始 
        MODULE
    }
    

    完整代码示例请前往 Github 查看: https://github.com/newobjectcc/code-example/tree/master/basic/annotation/AnnotaionDemo/src/target

    @Inherited

    通过 @Inherited 修饰的注解用在某个类上后,这个注解能被这个类的子类继承。

    但接口的实现类不能继承接口上 @Inherited 修饰的注解,以及类的方法并不从它所重载的方法继承注解(如果是继承父类中的方法,方法上的注解不管是否用 @Inherited 修饰的,注解随着方法一起被继承下来的)。

    完整代码示例请前往 Github 查看: https://github.com/newobjectcc/code-example/tree/master/basic/annotation/AnnotaionDemo/src/inherited

    @Repeatable

    被该元注解修饰的注解在同一位置是可重复使用的,从 Java8 版本开始支持。

    Java8 之前注解是不能像如下这样重复使用的。

    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyAnnotation {
    	String value();
    }
    
    @MyAnnotation ("A")
    @MyAnnotation ("B") // 此时重复使用会编译错误
    public class AnnotationUse {	
    }
    

    Java8 后开始支持重复使用,需要用 @Repeatable 来修饰注解。

    // 使用 @Repeatable 声明为可重复且指定一个容器
    @Repeatable(MyAnnotations.class) 
    @Retention(value = RetentionPolicy.RUNTIME)
    public @interface MyAnnotation {
        String value();
    }
    
    // 注解容器
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyAnnotations {
        MyAnnotation [] value();    
    }
    
    @MyAnnotation ("A")
    @MyAnnotation ("B") // 此时可以重复使用了
    public class AnnotationUse {    
    }
    

    如果对 AnnotationUse.class 反编译后发现,重复注解其实被隐式转换成:

    反编译class字节码

    所以 @Repeatable 才需要你指定一个注解容器,用来“存放”声明的注解,注解的容器中要定义一个要和存放类型一致的属性名为 value 的数组。

    @MyAnnotation 是我们定义的一个可重复注解,@MyAnnotations 是为这个注解定义的一个注解容器,那么在这两个注解上的元注解有一些需要注意的地方:

    对于 @Retention:
    注解容器上 @Retention 设置的生命周期至少要长于注解的生命周期。

    对于 @Inherited:
    如果注解为可继承的,那么注解容器也得声明为可继承的。反之,注解容器声明为可继承的,并不要求注解声明为可继承的。

    对于 @Target:
    如果注解上没有 @Target 元注解且注解容器也没有 @Target 元注解,则注解可以用任何支持该注解的元素上。
    如果注解上没有 @Target 元注解,但注解容器有 @Target 元注解,则注解只能在注解容器支持的元素上使用。
    如果注解上有 @Target 元注解,那么注解容器上的 @Target 值必须与注解上的@Target 种类值相同或为他的子集。但注解只能在注解容器支持的元素上使用。

    如果没什么特殊要求,以上的最好都保持一致。

    完整代码示例请前往 Github 查看: https://github.com/newobjectcc/code-example/tree/master/basic/annotation/AnnotaionDemo/src/repeatable

    @Documented

    被 @Documented 修饰的注解会被 javadoc 工具记录到文档中,默认情况下 javadoc 是不会将注解生成到类的文档上。

    @Documented
    public @interface MyAnnotation {}
    
    public @interface MyAnnotation2 {}
    
    @MyAnnotation
    @MyAnnotation2
    public class AnnotationUse {}
    

    通过 javadoc 生成文档。

    G:>javadoc -d doc AnnotationUse.java MyAnnotation.java MyAnnotation2.java
    

    javacdoc

    所以,添加 @Documented 元注解的 @MyAnnotation 注解出现在了 AnnotationUse 上,而没有添加 @Documented 的 @MyAnnotation2 没有出现,@Documented 起到了该作用。

    注解的面纱

    注解是一种特殊的接口类型,通过关键字 interface 前面加 @ 符号来声明注解,其实 @ 符号和关键字 interface是不同的标记,他们之间可以有空格的,但不建议这样做。

    public @  interface MyAnnotation {
    }
    

    每个注解类型都默认继承 java.lang.annotation.Annotation 接口。

    我们通过反编译一探究竟,对如下注解编译后的 .class 进行反编译。

    public @interface MyAnnotation {
    }
    

    反编译结果:

    反编译 class 字节码

    由反编译结果可以看的出来,注解确实是继承 Annotation 接口。

    既然是这样,我能否直接定义一个接口然后在继承 Annotation 接口呢。

    public interface MyAnnotation3 extends Annotation{
    }
    

    然后把它当做注解来用,就像这样:

    @MyAnnotation3
    public class AnnotationUse {
    }
    

    当然是不可以的,编译失败。

    编译 java 类

    提示 MyAnnotation3 不是注解类型,我们再次通过反编译 .class 字节码来对比一下。

    反编译 class 字节码

    MyAnnotation3.class 和 MyAnnotation.class 的反编译结果非常相似,差别是后者 flags 多出一个 ACC_ANNOTATION 标识。

    编译器在编译注解类时除了自动继承 Annotation 接口,还会给注解添加访问标志(access flags)ACC_ANNOTATION,标识他是一个注解类型。

    注解的本质就是继承 java.lang.annotation.Annotation 接口的特殊接口。

    这么看来,那么一个类能不能 implements 一个注解呢,试了下是可以的,比如这样:

    public @interface MyAnnotation {
    	String value();
     }
     
    public class Demo4 implements MyAnnotation{
    	@Override
    	public String value() {
    		return "hello world";
    	}
     
        public static void main(String[] args) {
    		System.out.println(new Demo4().value());
    	}
    	// ... 
    }
    

    当然这么做没什么意义,但也间接的从另一角度说明注解和接口是近亲关系。

    注解的解析

    类添加注解目的是提供元数据信息给编译器,开发工具,程序员等使用,这时就需要提取注解的信息(没有被任何处理和解析的注解就是代码垃圾),这时涉及到了注解的生命周期问题,@Retention 元注解可以设置三种生命周期 SOURCE,CLASS,RUNTIME。

    运行时只能读取生命周期为 RUNTIME 的注解,通过反射技术来对注解进行读取和解析。

    生命周期为 SOURCE 和 CLASS 的注解信息在运行时是读取不到的,这时在编译期间通过定义注解处理器(Annotation Processor)来对注解进行处理和解析,主要是用来实现检查性操作或生成某些辅助代码或文件。

    运行时注解解析

    运行时使用反射获取注解信息,反射相关类都各自实现了java.lang.reflect.AnnotatedElement 接口,该接口定义了获取注解信息相关方法。

    AnnotatedElement接口

    你可以通过这些方法访问具有运行时生命周期的注解。

    以获取类上注解为例:

    @MyAnnotation("类 AnnotationUse 上注解")
    public class AnnotationParse<@MyAnnotation T> {
        // ...
        public static void main(String[] args) {
              // ① 
              // System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
    
              MyAnnotation annotation = AnnotationParse.class.getAnnotation(MyAnnotation.class);
              // 输出结果为:类 AnnotationUse 上注解
              System.out.println(annotation.value());
              
             // 输出结果为:com.sun.proxy.$Proxy1
             // $Proxy1 是个代理类,这个 annotation 对象就是通过 JDK 代理生成的 @MyAnnotation(本质为接口)的一个代理类的对象
             // 想查看 class 字节码文件,可以设置一下系统属性将内存中的 class 字节码文件保存在本地,参考 ① 。
             System.out.println(annotation.getClass().getName());  
        }
    }
    

    完整代码示例请前往 Github 查看: https://github.com/newobjectcc/code-example/tree/master/basic/annotation/AnnotationParse/src/reflect

    编译期注解解析

    在 JDK5 中提供了 APT(Annotation Processing Tool) 工具来进行编译期的注解处理,它是 Oracle 提供的私有实现(包名都是 com.sun.mirror 开头的)。到 JDK6 时对注解处理器进行了规范化,提供了可插式注释处理 API(Pluggable Annotation Processing API),并增强了 javac 编译器API来使用注解处理器。JDK7 时 APT 功能已经被标准化的注解处理器取代,运行 APT 工具会打印一个警告,提示它将在下一个主要版本中被删除。JDK8 时已经移除了 APT 工具。

    定义可插式注释处理器通过继承 javax.annotation.processing.AbstractProcessor 类实现 process(...) 抽象方法,然后结合 javac 编译器来使用(编译器内部会调用注解处理器)。

    例如定义一个简单的检查方法命名的注解处理器:

    完整代码示例请前往 Github 查看: https://github.com/newobjectcc/code-example/tree/master/basic/annotation/AnnotationProcessor

    使用注解处理器

    1. 通过 javac -processor <class> 在编译时指定调用的注解处理器。

    首先编译注解和注解处理器。

    编译注解和注解处理器

    使用注解处理器。

    使用注解处理器

    如上,编译时注解处理器执行了,输出了警告信息。

    1. 除此之外,当不指定 -processor 参数选项时,会默认通过 SPI(Service Provider Interface) 机制调用注解处理器,前提是得根据约定正确的配置 SPI,需要创建一个 META-INFservices 目录和注解处理器接口名称的文件,文件中指定注解处理器实现。

    通过SPI机制使用注解处理器

    然后将项目打成 jar 包,然后编译 Annotation.java。
    打成jar包

    同样,编译时注解处理器执行了且输出了警告信息。

    1. 可以把注解处理器集成到开发工具,以 Eclipse 为例:

    集成到Eclipse
    集成到Eclipse
    集成到Eclipse

    查看效果:

    集成到Eclipse

    该例中的注解处理器作用是检测方法名是否符合命名规范。

    如果需要编译过程修改 class 内容,比如编译时根据属性自动生成 getter ,setter 方法等,这涉及到修改 AST(抽象语法树)。著名的 Lombok(一个 java 库,只需类上添加注解,编译后就可以自动生成 getter,setter,equals,构造方法以及自动化日志变量等)就是基于注解处理器修改 AST 来实现的。

    小结

    注解相当于贴在类上的标签,用来为类、接口、方法和字段等提供元数据信息,使编译器、其他工具,程序等读取元数据来执行某些操作。

    注解本质是一种特殊的接口类型,注解中的方法声明返回类型是有限制的,必须规定的几种类型之一。

    JDK提供了 @Retention,@Target,@Inherited,@Repeatable,@Documented 等元注解。

    在运行时通过反射技术解析生命周期为 RUNTIME 的注解,生命周期为 SOURCE 和 CLASS的注解在运行时读取不到的,在编译期间通过定义注解处理器来对注解进行处理和解析。

    注解处理器功能非常强大,可以在编译期间修改语法树,改变生成的字节码文件。


    参考:
    https://jcp.org/en/jsr/detail?id=175
    https://jcp.org/en/jsr/detail?id=269
    https://jcp.org/en/jsr/detail?id=270
    https://docs.oracle.com/javase/specs/jls/se12/html/jls-9.html#jls-9.6
    https://docs.oracle.com/javase/specs/jvms/se12/html/jvms-4.html#jvms-4.1
    https://docs.oracle.com/javase/1.5.0/docs/guide/language/annotations.html
    https://docs.oracle.com/javase/1.5.0/docs/guide/apt/index.html
    https://www.oracle.com/technetwork/java/javase/compatibility-417013.html
    https://www.oracle.com/technetwork/java/javase/8-compatibility-guide-2156366.html
    https://stackoverflow.com/questions/17237813/elementtype-local-variable-annotation-type
    https://yq.aliyun.com/articles/704117

    作者:陆十三
    转载请在明显位置注明出处!
  • 相关阅读:
    如何编写CMakeLists.txt
    C++11 condition_variable
    TCP/IP 收发数据
    CLion 远程开发和调试C/C++代码
    Python unittest mock 在哪里打patch
    MVCC版本链
    数据无法修改?解密MVCC原理
    MVCC ReadView介绍
    正则表达式备忘(基于JavaScript)
    《C+编程规范 101条规则、准则与最佳实践》笔记
  • 原文地址:https://www.cnblogs.com/newobjectcc/p/14290474.html
Copyright © 2020-2023  润新知