什么是注解
注解(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 接口包含如下方法:
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 反编译后发现,重复注解其实被隐式转换成:
所以 @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
所以,添加 @Documented 元注解的 @MyAnnotation 注解出现在了 AnnotationUse 上,而没有添加 @Documented 的 @MyAnnotation2 没有出现,@Documented 起到了该作用。
注解的面纱
注解是一种特殊的接口类型,通过关键字 interface 前面加 @ 符号来声明注解,其实 @ 符号和关键字 interface是不同的标记,他们之间可以有空格的,但不建议这样做。
public @ interface MyAnnotation {
}
每个注解类型都默认继承 java.lang.annotation.Annotation 接口。
我们通过反编译一探究竟,对如下注解编译后的 .class 进行反编译。
public @interface MyAnnotation {
}
反编译结果:
由反编译结果可以看的出来,注解确实是继承 Annotation 接口。
既然是这样,我能否直接定义一个接口然后在继承 Annotation 接口呢。
public interface MyAnnotation3 extends Annotation{
}
然后把它当做注解来用,就像这样:
@MyAnnotation3
public class AnnotationUse {
}
当然是不可以的,编译失败。
提示 MyAnnotation3 不是注解类型,我们再次通过反编译 .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 接口,该接口定义了获取注解信息相关方法。
你可以通过这些方法访问具有运行时生命周期的注解。
以获取类上注解为例:
@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
使用注解处理器
- 通过
javac -processor <class>
在编译时指定调用的注解处理器。
首先编译注解和注解处理器。
使用注解处理器。
如上,编译时注解处理器执行了,输出了警告信息。
- 除此之外,当不指定 -processor 参数选项时,会默认通过 SPI(Service Provider Interface) 机制调用注解处理器,前提是得根据约定正确的配置 SPI,需要创建一个 META-INF\services 目录和注解处理器接口名称的文件,文件中指定注解处理器实现。
然后将项目打成 jar 包,然后编译 Annotation.java。
同样,编译时注解处理器执行了且输出了警告信息。
- 可以把注解处理器集成到开发工具,以 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