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)

注解的用途

注解在大多数时候,它的用途就是『简化代码,让你专注做重要的事情』。当然,不同的注解有不同的功能,下面列出四个主要的功能:

  1. 生成文档,通过代码里标识的元数据生成 Javadoc 文档。
  2. 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证。
  3. 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码。
  4. 运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例。

下面我们用一个栗子,来介绍一下注解的基本原理。

注解的实现原理

// 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 信息:

  1. <T extends Annotation> T getAnnotation(Class<T> annotationClass): 返回改程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回 null。
  2. Annotation[] getAnnotations():返回该程序元素上存在的所有注解
  3. Annotation[] getDeclaredAnnotations():返回直接存在于此元素上的所有注解。与第2个方法不同的地方在于,该方法将忽略继承的注解(如果没有注解直接存在于此元素上,则返回长度为零的一个数组)。
  4. 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 实现原理解析》,来作为注解使用的最佳解析。