Annotation 是 JDK1.5 之后加入到 Java 中的,它其实就是代码里的特殊标记, 用于替代配置文件,也就是说,传统方式通过配置文件告诉类如何运行,有了注解技术后,开发人员可以通过注解告诉类如何运行。它提供了一种安全的类似注释的机制,用来将信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。它像一种修饰符一样,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中。
注解是以@注解名
在代码中存在的,根据注解参数的个数,我们可以将注解分为:
- 标记注解
- 单值注解
- 完整注解
它们都不会直接影响到程序的语义,只是作为注解(标识)存在,我们可以通过反射机制编程实现对这些元数据(用来描述数据的数据)的访问。
下图展示了 Java 中常见的注解的分类:
标准注解
Java中有三个最基本的标准注解:
@Override
: 限定重写父类方法, 该注解只能用于方法。@Deprecated
: 用于表示某个程序元素(类, 方法等)已过时。@SuppressWarnings
: 抑制编译器警告。
注解属性的类型只能是如下类型:String 类型、8大基本数据类型(int, float, boolean, byte, double, char, long, short)、Class类型、枚举类型、注解类型以及以上类型的一维数组。如果一个注解只有一个属性,并且这个属性的名称为value
的话,那么使用注解时可以省略value=
部分,如将@MyAnnotation(value="XXX")
替换为@MyAnnotation("xxx")
。
元注解
提到注解,也不得不提一下『元注解』。元注解即『注解的注解』,专门负责注解其他的注解。在Java中有几种常用的元注解,分别如下:
@Retention
:标识什么时候使用该注解。@Documented
:注解是否将包含在Javadoc中。@Target
:注解将用于什么地方。@Inherited
:是否允许子类继承该注解。@Repeatable
:允许对某个元素多次使用同一个注解。
其中,Repeatable 是在 jdk1.8 中引进的。我们简单地解释一下这几种元注解。
1. Retention
Retention 元注解,表示需要在什么级别保存该注解信息(生命周期)。如果注解类型声明中不存在 Retention 注解,则保留策略默认为 RetentionPolicy.CLASS
。可选的 RetentionPoicy 参数包括:
枚举常量 | 描述 |
---|---|
RetentionPolicy.SOURCE |
停留在 java 源文件,会被编译器丢弃 |
RetentionPolicy.CLASS |
停留在 class 文件中,但会被VM丢弃(默认) |
RetentionPolicy.RUNTIME |
内存中的字节码,VM 将在运行时也保留注解,因此可以通过反射机制读取注解的信息 |
2. Documented
Documented 将注解包含在 Javadoc 中。
使用该注解,表示某一个类型的注释将通过 Javadoc 或者类似的工具进行文档化,应使用此类型来注释这些类型的声明,其注释会影响由其客户端注释的元素的使用。如果类型声明是用 Documented 来注释的,则其注释将成为注释元素的公共 API 的一部分。
3. Target
Target 说明了注解所修饰的对象范围:Annotation 可被用于 packages、types(类、接口、枚举、Annotation 类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。在 Annotation 类型的声明中使用了 Target 可更加明晰其修饰的目标。只有元注解类型直接用于注解时,Target 元注释才有效。如果元注解类型用作另一种注解类型的成员,则无效。如果注释类型声明中不存在 Target 元注释,则声明的类型可以用在任一程序元素上。如果存在这样的元注释,则编译器强制实施指定的使用限制。
枚举常量 | 描述 |
---|---|
ElementType.CONSTRUCTOR |
构造器声明 |
ElementType.FIELD |
成员变量、对象、属性(包括enum实例) |
ElementType.LOCAL_VARIABLE |
局部变量声明 |
ElementType.METHOD |
方法声明 |
ElementType.PACKAGE |
包声明 |
ElementType.PARAMETER |
参数声明 |
ElementType.TYPE |
类、接口(包括注解类型)或enum声明 |
举个例子,Retrofit2中有个@Field
注解,我们看看它的源码:
// Field.java
import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Documented
@Target(PARAMETER)
@Retention(RUNTIME)
public @interface Field {
String value();
/** Specifies whether the {@linkplain #value() name} and value are already URL encoded. */
boolean encoded() default false;
}
其 Target 注解的常量类型为PARAMETER
,表示Field
这个注解适用于参数,如果这个注解用在了方法上,那就不起任何作用。
4. Inherited
Inherited 允许子类继承父类中的注解,注释类型被自动继承。
如果在注释类型声明中存在 Inherited 元注释,并且用户在某一类声明中查询该注释类型,同时该类声明中没有此类型的注释,则将在该类的超类中自动查询该注释类型。此过程会重复进行,直到找到此类型的注释或到达了该类层次结构的顶层 (Object) 为止。如果没有超类具有该类型的注释,则查询将指示当前类没有这样的注释。如果使用注释类型注释类以外的任何事物,此元注释类型都是无效的。此元注释仅促成从超类继承注释,对已实现接口的注释无效。
5. Repeatable
Repeatable 表示被标记的注解可以多次应用于相同的声明或类型。举例如下:
先声明一个重复注解类:
@Repeatable(Tip.class)
public @interface Tip {
String dayOfMonth() default "1";
String dayOfWeek() default "Monday";
int hour() default 12;
}
再声明一个容器注解类:
@Retention(RetentionPolicy.RUNTIME)
public @interface Tips {
Tip[] value();
}
然后是使用:
@Tip(dayOfMonth = "10")
@Tip(dayOfWeek = "Tuesday", hour = 15)
public class RepetableAnnotation{
@Tip(dayOfMonth = "20")
@Tip(dayOfWeek = "Friday", hour = 23)
public void doPeriodicTip(){}
public static void main(String[] args) throws NoSuchMethodException {
Method doPeriodicTip = RepetableAnnotation.class.getMethod("doPeriodicTip");
Tips tips = doPeriodicTip.getAnnotation(Tips.class);
System.out.println("获取标记方法上的重复注解:");
for (Tip tip: tips.value()){
System.out.println(tip);
}
System.out.println("获取标记类上的重复注解:");
if (RepetableAnnotation.class.isAnnotationPresent(Tips.class)){
Tips = RepetableAnnotation.class.getAnnotation(Tips.class);
for (Tip Tip: Tips.value()){
System.out.println(Tip);
}
}
}
}
运行结果如下:
获取标记方法上的重复注解:
Tip(hour=12, dayOfMonth=10, dayOfWeek=Monday)
Tip(hour=23, dayOfMonth=1, dayOfWeek=Friday)
获取标记类上的重复注解:
Tip(hour=12, dayOfMonth=10, dayOfWeek=Monday)
Tip(hour=15, dayOfMonth=1, dayOfWeek=Tuesday)
注解的用途
注解在大多数时候,它的用途就是『简化代码,让你专注做重要的事情』。当然,不同的注解有不同的功能,下面列出四个主要的功能:
- 生成文档,通过代码里标识的元数据生成 Javadoc 文档。
- 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证。
- 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码。
- 运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例。
下面我们用一个栗子,来介绍一下注解的基本原理。
注解的实现原理
// CustomTargetType.java
/**
* 定义一个可以注解在 class,interface,enum 上的注解
*/
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomTargetType {
String value() default "我是定义在类、接口、枚举类上的注解元素 value 的默认值";
}
// CustomTargetField.java
/**
* 定义一个可以注解在FIELD上的注解
*/
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomTargetField {
String value() default "我是定义在字段上的注解元素 value 的默认值";
}
// CustomTargetMethod.java
/**
* 定义一个可以注解在METHOD上的注解
*/
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomTargetMethod {
String value() default "我是定义在方法上的注解元素 value 的默认值";
}
// CustomTargetParameter.java
/**
* 定义一个可以注解在PARAMETER上的注解
*/
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomTargetParameter {
String value() default "我是定义在参数上的注解元素 value 的默认值";
}
注解定义完毕后,我们写一个测试类来处理上面的注解:
// AnnotationTest.java
@CustomTargetType
public class AnnotationTest {
@CustomTargetField
private String mField = "我是字段";
@CustomTargetMethod("测试方法")
public void test(@CustomTargetParameter String args) {
System.out.println("参数值 === " + args);
}
public static void main(String[] args) {
// 获取类上的注解 CustomTargetType
CustomTargetType t = AnnotationTest.class.getAnnotation(CustomTargetType.class);
System.out.println("类上的注解值 === " + t.value());
CustomTargetMethod tm = null;
try {
// 根据反射获取 AnnotationTest 类上的 test 方法
Method method = AnnotationTest.class.getDeclaredMethod("test",String.class);
// 获取方法上的注解 CustomTargetMethod
tm = method.getAnnotation(CustomTargetMethod.class);
System.out.println("方法上的注解值 === " + tm.value());
// 获取方法上的所有参数注解,循环所有注解找到 CustomTargetParameter 注解
Annotation[][] annotations = method.getParameterAnnotations();
for(Annotation[] tt : annotations){
for(Annotation t1:tt){
if(t1 instanceof CustomTargetParameter){
System.out.println("参数上的注解值 === "+((CustomTargetParameter) t1).value());
}
}
}
method.invoke(new AnnotationTest(), "改变默认参数");
// 获取 AnnotationTest 类上字段 mField 的注解 CustomTargetField
CustomTargetField fieldAn = AnnotationTest.class.getDeclaredField("mField").getAnnotation(CustomTargetField.class);
System.out.println("字段上的注解值 === " + fieldAn.value());
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果如下:
类上的注解值 === 我是定义在类、接口、枚举类上的注解元素 value 的默认值
参数上的注解值 === 我是定义在参数上的注解元素 value 的默认值
参数值 === 改变默认参数
方法上的注解值 === 测试方法
字段上的注解值 === 我是定义在字段上的注解元素 value 的默认值
它们是如何工作的呢?
我们观察上方的栗子,不难看出,每个注解有三个要素:
- 注解声明
- 使用注解的元素
- 操作注解使其生效 - 由注解处理器来完成
注解声明和使用注解的元素不必多说,我们详细讲讲注解处理器是如何处理注解的。
注解处理器
::: tips 什么是注解处理器?
注解处理器是(Annotation Processor)是 javac 的一个工具,用来在编译时扫描和处理注解。你可以自定义注解处理器去处理一些事务。一个注解处理器它以 java 代码或者编译过的字节码作为输入,生成文件(通常是 java 文件)。这些生成的 java 文件不能修改,并且会同其手动编写的 java 代码一样会被 javac 编译。说白了就是把标记了注解的类、变量等作为输入内容,经过注解处理器处理,生成想要生成的 java 代码。
:::
AbstractProcessor
处理器的写法有固定的套路,继承AbstractProcessor。如下:
public abstract class AbstractProcessor implements Processor {
// 使用 processingEvn 初始化处理器
// 如果同一个对象的该方法被调用多次,将会抛出 IllegalStateException 异常
public synchronized void init(ProcessingEnvironment processingEnv) {
if (initialized)
throw new IllegalStateException("Cannot call init more than once.");
Objects.requireNonNull(processingEnv, "Tool provided null ProcessingEnvironment");
this.processingEnv = processingEnv;
initialized = true;
}
// 如果注解处理器类被 @SupportedAnnotationTypes 注解过,则返回该注解处理器支持的注解类的名称 Set
// 如果没有,就返回一个空 Set
public Set<String> getSupportedAnnotationTypes() {
SupportedAnnotationTypes sat = this.getClass().getAnnotation(SupportedAnnotationTypes.class);
if (sat == null) {
if (isInitialized())
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
"No SupportedAnnotationTypes annotation " +
"found on " + this.getClass().getName() +
", returning an empty set.");
return Collections.emptySet();
}
else
return arrayToSet(sat.value());
}
// 如果注解处理器类被 @SupportedSourceVersion 注解过,则返回该注解支持的最小 source version
// 如果没有,返回 SourceVersion#RELEASE_6
public SourceVersion getSupportedSourceVersion() {
SupportedSourceVersion ssv = this.getClass().getAnnotation(SupportedSourceVersion.class);
SourceVersion sv = null;
if (ssv == null) {
sv = SourceVersion.RELEASE_6;
if (isInitialized())
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
"No SupportedSourceVersion annotation " +
"found on " + this.getClass().getName() +
", returning " + sv + ".");
} else
sv = ssv.value();
return sv;
}
// 主要的处理过程,重写这个方法来扫描并处理你的注解,生成 java 代码
public abstract boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv);
}
我们会在这篇文章里详细解释如何使用自定义注解。
::: details 废弃
注解处理器
不同的程序元素,其注解处理器的处理方式不太一样,我们需要各个分析。
Class 注解处理器
我们从AnnotationTest.class.getAnnotation()
方法开始入手。下方代码基于 jdk1.8:
// Class.java
@SuppressWarnings("unchecked")
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {
Objects.requireNonNull(annotationClass);
return (A) annotationData().annotations.get(annotationClass);
}
瞧,这个方法本身居然还使用了@SuppressWarnings
注解呢。
这个方法是实现了java.lang.reflect.AnnotatedElement
接口中的getAnnotation()
方法:
// java.lang.reflect.AnnotatedElement.java
// 如果元素有注解的话,返回元素的注解,否则返回 null。
<T extends Annotation> T getAnnotation(Class<T> annotationClass);
java.lang.reflect.AnnotatedElement
接口是所有程序元素(Class、Method 和 Constructor)的父接口,所以程序通过反射获取了某个类的 AnnotatedElement 对象之后,程序就可以调用该对象的如下 4 个方法来访问 Annotation 信息:
<T extends Annotation> T getAnnotation(Class<T> annotationClass)
: 返回改程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回 null。Annotation[] getAnnotations()
:返回该程序元素上存在的所有注解。Annotation[] getDeclaredAnnotations()
:返回直接存在于此元素上的所有注解。与第2个方法不同的地方在于,该方法将忽略继承的注解(如果没有注解直接存在于此元素上,则返回长度为零的一个数组)。boolean is AnnotationPresent(Class<?extends Annotation> annotationClass)
:判断该程序元素上是否包含指定类型的注解,存在则返回true
,否则返回false
。
然后接着分析上面的代码:
// Class.java
// Annotations cache
@SuppressWarnings("UnusedDeclaration")
private volatile transient AnnotationData annotationData;
private AnnotationData annotationData() {
while (true) { // 重复获取直到成功
AnnotationData annotationData = this.annotationData;
int classRedefinedCount = this.classRedefinedCount;
// 判断是否为旧注解
// 因为 JVM 调用 RedefineClass() 方法时,annotationData 的值可能会发生改变,
// 此时,必须要刷新一次 annotationData
if (annotationData != null &&
annotationData.redefinedCount == classRedefinedCount) {
return annotationData;
}
// 空注解或旧注解,就创建新的注解实例
AnnotationData newAnnotationData = createAnnotationData(classRedefinedCount);
// 将新建的 annotationData 关联到这个类上
// 最终会调用到 native 层的 compareAndSwapObject
if (Atomic.casAnnotationData(this, annotationData, newAnnotationData)) {
return newAnnotationData;
}
}
}
这段代码旨在将类的注解取出,并放在一个用 transient 修饰的变量中,用于缓存。
第一次调用该方法时,annotationData 为 null,则需要新建 annotationData 的实例,并关联到类上。
AnnotationData 类的声明如下:
// annotation data that might get invalidated when JVM TI RedefineClasses() is called
private static class AnnotationData {
// 用以存储 annotations 的 map
final Map<Class<? extends Annotation>, Annotation> annotations;
final Map<Class<? extends Annotation>, Annotation> declaredAnnotations;
// 记录创建了几次 annotationData,用于与 Class.classRedefinedCount 进行对比,
// 以决定是否要刷新 annotationData
final int redefinedCount;
AnnotationData(Map<Class<? extends Annotation>, Annotation> annotations,
Map<Class<? extends Annotation>, Annotation> declaredAnnotations,
int redefinedCount) {
this.annotations = annotations;
this.declaredAnnotations = declaredAnnotations;
this.redefinedCount = redefinedCount;
}
}
最后来梳理一下整个反射注解的工作原理:
首先,我们通过键值对的形式可以为注解属性赋值,像这样:@Hello(value = “hello”)。
接着,你用注解修饰某个元素,编译器将在编译期扫描每个类或者方法上的注解,会做一个基本的检查,你的这个注解是否允许作用在当前位置,最后会将注解信息写入元素的属性表。
然后,当你进行反射的时候,虚拟机将所有生命周期在 RUNTIME 的注解取出来放到一个 map 中,并创建一个 AnnotationInvocationHandler 实例,把这个 map 传递给它。
最后,虚拟机将采用 JDK 动态代理机制生成一个目标注解的代理类,并初始化好处理器。
:::
我也写了一篇单独的文章 - 《Retrofit 实现原理解析》,来作为注解使用的最佳解析。