简介
Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。 Java 语言中的类、方法、变量、参数和包等都可以被标注。 注解是元数据的一种形式,提供有关于程序但不属于程序本身的数据。注解对它们注解的代码的操作没有直接影响。
注解的结构
- 一个Annotation和一个
RetentionPolicy
关联 - 一个Annotation和一个或者多个
ElementType
关联 - Annotation有许多实现类
Annotation 的源码组成
从Java的源码上看Annotation的组成,有三个重要的主干类: Annotation.java
java
package java.lang.annotation;
public interface Annotation {
/** 判断指定对象与当前注解是否逻辑一致 */
boolean equals(Object obj);
/** 计算并返回当前注解的哈希码 */
int hashCode();
/** 返回当前注解的字符串表示 */
String toString();
/** 返回当前注解对应的注解类型,避免因注解实现类依赖问题,能可靠获取注解类型。 */
Class<? extends Annotation> annotationType();
}
ElementType.java
java
package java.lang.annotation;
public enum ElementType {
/** 类、接口(包括注解类型)、枚举或记录声明 */
TYPE,
/** 字段声明(包括枚举常量) */
FIELD,
/** 方法声明 */
METHOD,
/** 形式参数声明 */
PARAMETER,
/** 构造函数声明 */
CONSTRUCTOR,
/** 局部变量声明 */
LOCAL_VARIABLE,
/** 注解类型声明 */
ANNOTATION_TYPE,
/** 包声明 */
PACKAGE,
/** 类型参数声明 */
TYPE_PARAMETER,
/** 类型的使用 */
TYPE_USE,
/** 模块声明 */
MODULE,
/** 记录组件 */
RECORD_COMPONENT;
}
RetentionPolicy.java
java
package java.lang.annotation;
public enum RetentionPolicy {
/** 注解仅在源码阶段存在,编译器会将其丢弃,不会保留在编译后的字节码文件中。 */
SOURCE,
/** 注解会被编译器记录在字节码文件中,但在运行时虚拟机不需要保留这些注解。这是注解的默认保留策略。 */
CLASS,
/** 注解会被编译器记录在字节码文件中,并且在运行时会被虚拟机保留,因此可以通过反射机制读取这些注解。 */
RUNTIME
}
从源码中可以看出:
-
每个 Annotation 对象,都会有唯一的
RetentionPolicy
属性;而ElementType
属性,则有 1~n 个。 -
ElementType
是 Enum 枚举类型,它用来指定 Annotation 的应用类型。当 Annotation 与某个ElementType
关联时,就意味着:Annotation有了某种用途。 例如,若一个 Annotation 对象是 METHOD 类型,则该 Annotation 只能用来修饰方法。 -
RetentionPolicy
是 Enum 枚举类型,它用来指定 Annotation 的作用域。 (1) 若 Annotation 的类型为 SOURCE,Annotation 仅存在于编译器处理期间 ,编译器处理完之后,该 Annotation 就没用了。 例如," @Override" 标志就是一个 Annotation。当它修饰一个方法的时候,就意味着该方法覆盖父类的方法;并且在编译期间会进行语法检查。编译器处理完后,"@Override" 就没有任何作用了。 (2) 若 Annotation 的类型为 CLASS,编译器将 Annotation 存储于类对应的.class
文件中,它是 Annotation 的默认行为。 (3) 若 Annotation 的类型为 RUNTIME,编译器将 Annotation 存储于class
文件中,并且可由JVM读入。
注解的类型
Java内置了多个注解,其中3个在java.lang
中(如@Override),其余如@Target、@Retention等位于java.lang.annotation包
中
用于代码的注解
@Override
:只能用于方法。表示覆盖父类的方法。如果发现其父类,或者引用的接口中并没有该方法时,会报编译错误。
@Deprecated
:标记过时方法。如果使用该方法,会警告。
@SuppressWarnings
:忽略警告。
用于注解的注解(元注解)
注解类也能够使用其他的注解声明。 对注解类型进行注解的注解类,我们称之为meta-annotation (元注解)。一般的,我们在定义自定义注解时,需要指定的元注解有两个 :@Target
和 @Retention
@ Target:限制这个注解应该用于哪种 Java 成员。(不使用则默认无限制),可以选取下面的一个或者多个作为其值。
ElementType.ANNOTATION_TYPE
应用于注解类型。
ElementType.CONSTRUCTOR
应用于构造函数。
ElementType.FIELD
应用于字段或属性。
ElementType.LOCAL_VARIABLE
应用于局部变量。
ElementType.METHOD
应用于方法级注解。
ElementType.PACKAGE
应用于包声明。
ElementType.PARAMETER
应用于方法的参数。
ElementType.TYPE
应用于类的任何元素。
@Retention:这个注解最多保存到什么时候
RetentionPolicy.SOURCE
仅保留在源级别中,并被编译器忽略。
RetentionPolicy.CLASS
在编译时由编译器保留,但JVM会忽略。
RetentionPolicy.RUNTIME
由 JVM 保留。
从保留的时间长短上看: SOURCE < CLASS < RUNTIME
其他的有: @Documented
:标记这些注解是否包含在用户文档(javadoc)中。 @Inherited
:表示注解可被继承,父类使用的注解若带有@Inherited,子类会自动继承该注解。
从 Java 7 开始,额外添加了 3 个注解: @SafeVarargs
:Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。 @FunctionalInterface
:Java 8 开始支持,标识一个匿名函数或函数式接口。 @Repeatable
:Java 8 开始支持,标识某注解可以在同一个声明上使用多次。
注解的使用
基本声明
一个注解的声明使用@interface
关键字,通用的注解声明如下:
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationTest0{
}
@interface : Java中的所有注解,都默认实现
java.lang.annotation.Annotation
接口。这是必须的。与implemented方法不同,使用@interface后不能继承其他注解或者接口。
注解类型元素
在上文元注解中,允许在使用注解时传递参数。我们也能让自定义注解的主体包含 annotation type element (注解类型元素) 声明,它们看起来很像方法,可以定义可选的默认值。
java
@Target({ElementType.TYPE,ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface AnnotationTest1{
String value(); //无默认值
int age() default 1; //有默认值
}
注解元素的类型只能是基本类型、String、Class、枚举、其他注解或这些类型的数组,且不能为null。
若注解仅有一个名为value()的元素,使用时可以省略元素名,直接传入值。
java
@AnnotationTest1("帅") //如果只存在value元素需要传值的情况,则可以省略:元素名=
@AnnotationTest1(value="帅",age = 2)
int i;
注意:在使用注解时,如果定义的注解中的类型元素无默认值,则必须进行传值。
应用场景
按照 @Retention 定义的注解存储方式,可以分为三种使用场景
RetentionPolicy | 存储位置 | 是否可反射获取 | 典型应用场景 |
---|---|---|---|
SOURCE | 仅源码 | 否 | 静态检查、代码生成 |
CLASS | class文件 | 否 | 字节码增强、AOP |
RUNTIME | class文件+JVM内存 | 是 | 运行时动态处理 |
SOURCE
作用在源码上的注解,可以用作IDE语法检查,APT等场景。
取代枚举
Java中Enum(枚举)的实质是特殊单例的静态成员变量,在运行期所有枚举类作为单例,全部加载到内存中。比常量多5到10倍的内存占用。
例如: 我们定义方法 test ,此方法接收参数 teacher 需要在:LI、WANG中选择一个。如果使用枚举能够实现为:
java
public enum Teacher{
LI,WANG
}
public void test(Teacher teacher) {
}
而现在为了进行内存优化,我们现在不再使用枚举,则方法定义为:
java
public static final int LI = 1;
public static final int WANG = 2;
public void test(int teacher) {
}
然而此时,调用 test 方法由于采用基本数据类型int,将无法进行类型限定。此时使用@IntDef
增加自定义注解:
java
public static final int LI = 1;
public static final int WANG = 2;
@IntDef(value = {LI, WANG}) //限定为LI,WANG
@Target(ElementType.PARAMETER) //作用于参数的注解
@Retention(RetentionPolicy.SOURCE) //源码级别注解
public @interface Teacher {
}
public void test(@Teacher int teacher) {
}
@IntDef和@StringDef属于Android支持库(androidx.annotation)中的特有注解,需添加依赖:implementation 'androidx.annotation:annotation:1.7.0'
此时,我们再去调用 test 方法,如果传递的参数不是 LI 或者 WANG 则会显示 Inspection 警告(编译不会报错)。 以上注解均为 SOURCE 级别,本身IDEA/AS 就是由Java开发的,Lint工具实现了对Java语法的检查,借助注解能对被注解的特定语法进行额外检查。
APT
APT全称为:"Anotation Processor Tools",意为注解处理器。顾名思义,其用于处理注解。编写好的Java源文件,需要经过 javac 的编译,翻译为虚拟机能够加载解析的字节码Class文件。注解处理器是 javac 自带的一个工具,用来在编译时期扫描处理注解信息。你可以为某些注解注册自己的注解处理器。 注册的注解处理器由 javac调起,并将注解信息传递给注解处理器进行处理。
注解处理器是对注解应用最为广泛的场景。在Glide、EventBus3、Butterknifer、Tinker、ARouter等等常用框架中都有注解处理器的身影。但是你可能会发现,这些框架中对注解的定义并不是SOURCE 级别,更多的是 CLASS 级别,别忘了:CLASS包含了SOURCE,RUNTIME包含SOURCE、CLASS。
CLASS
定义为 CLASS 的注解,会保留在class文件中,但是会被虚拟机忽略(即无法在运行期反射获取注解)。此时完全符合此种注解的应用场景为字节码操作。如:AspectJ、热修复Roubust中应用此场景。
所谓字节码操作即为,直接修改字节码Class文件以达到修改代码执行逻辑的目的。在程序中有多处需要进行是否登录的判断。
例如验证登录操作。如果我们使用普通的编程方式,需要在代码中进行 if-else 的判断,也许存在十个判断点,则需要在每个判断点加入此项判断。此时,我们可以借助AOP(面向切面)编程思想,将程序中所有功能点划分为: 需要登录 与 无需登录 两种类型,即两个切面。对于切面的区分即可采用注解。
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Login {
}
@Login
public void jumpA(){
startActivity(new Intent(this,AActivity.class));
}
public void jumpB(){
startActivity(new Intent(this,BActivity.class));
}
在上述代码中, jumpA 方法需要具备登录身份。而 Login 注解的定义被设置为 CLASS 。因此我们能够在该类所编译的字节码中获得到方法注解 Login 。在操作字节码时,就能够根据方法是否具备该注解来修改class中该方法的内容加入 if-else 的代码段:
java
@Login
public void jumpA() {
if (this.isLogin) {
this.startActivity(new Intent(this, LoginActivity.class));
} else {
this.startActivity(new Intent(this, AActivity.class));
}
}
public void jumpB() {
startActivity(new Intent(this,BActivity.class));
}
注解能够设置类型元素(参数),结合参数能实现更为丰富的场景,如:运行期权限判定等。
RUNTIME
注解保留至运行期,意味着我们能够在运行期间结合反射技术获取注解中的所有信息。