NO编程

NO编程

爱编程,爱分享,爱生活

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-INF\services 目录和注解处理器接口名称的文件,文件中指定注解处理器实现。

通过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

posted @ 2021-01-18 10:27  陆十三  阅读(810)  评论(0编辑  收藏  举报