Toc
  1. 标准注解
  2. 元注解
    1. 1. Retention
    2. 2. Documented
    3. 3. Target
    4. 4. Inherited
    5. 5. Repeatable
  3. 注解的用途
  4. 注解的实现原理
  5. 注解处理器
    1. AbstractProcessor
  6. 注解处理器
    1. Class 注解处理器
Toc
0 results found
关于注解
2020/02/03 Development Java 注解

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 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 annotations,
RoundEnvironment roundEnv);
}

我们会在 这篇文章 里详细解释如何使用自定义注解。

::: details 废弃

注解处理器

不同的程序元素,其注解处理器的处理方式不太一样,我们需要各个分析。

Class 注解处理器

我们从 AnnotationTest.class.getAnnotation() 方法开始入手。下方代码基于 jdk1.8

// Class.java

@SuppressWarnings("unchecked")
public A getAnnotation(Class annotationClass) {
Objects.requireNonNull(annotationClass);

return (A) annotationData().annotations.get(annotationClass);
}

瞧,这个方法本身居然还使用了 @SuppressWarnings 注解呢。

这个方法是实现了 java.lang.reflect.AnnotatedElement 接口中的 getAnnotation() 方法:

// java.lang.reflect.AnnotatedElement.java

// 如果元素有注解的话,返回元素的注解,否则返回 null。
T getAnnotation(Class annotationClass);

java.lang.reflect.AnnotatedElement接口是所有程序元素(Class、Method 和 Constructor)的父接口,所以程序通过反射获取了某个类的 AnnotatedElement 对象之后,程序就可以调用该对象的如下 4 个方法来访问 Annotation 信息:

  1. T getAnnotation(Class annotationClass): 返回改程序元素上 存在的、指定类型的注解,如果该类型注解不存在,则返回 null。
  2. Annotation[] getAnnotations(): 返回该程序元素上存在的 所有注解
  3. Annotation[] getDeclaredAnnotations():返回直接存在于此元素上的 所有注解 。与第 2 个方法不同的地方在于, 该方法将忽略继承的注解(如果没有注解直接存在于此元素上,则返回长度为零的一个数组)。
  4. boolean is AnnotationPresent(Class 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, Annotation> annotations;
final Map, Annotation> declaredAnnotations;

// 记录创建了几次 annotationData,用于与 Class.classRedefinedCount 进行对比,
// 以决定是否要刷新 annotationData
final int redefinedCount;

AnnotationData(Map, Annotation> annotations,
Map, Annotation> declaredAnnotations,
int redefinedCount) {
this.annotations = annotations;
this.declaredAnnotations = declaredAnnotations;
this.redefinedCount = redefinedCount;
}
}

最后来梳理一下整个反射注解的工作原理:

首先,我们通过 键值对 的形式可以 为注解属性赋值,像这样:@Hello(value = "hello")。

接着,你用注解修饰某个元素,编译器将在编译期扫描每个类或者方法上的注解,会做一个基本的检查,你的这个注解是否允许作用在当前位置,最后会将注解信息 写入元素的属性表

然后,当你进行反射的时候,虚拟机将所有生命周期在 RUNTIME 的注解取出来放到一个 map 中,并创建一个 AnnotationInvocationHandler 实例,把这个 map 传递给它。

最后,虚拟机将采用 JDK 动态代理机制生成一个目标注解的代理类,并初始化好处理器。
:::

我也写了一篇单独的文章 - 《Retrofit 实现原理解析》,来作为注解使用的最佳解析。

打赏
支付宝
微信
本文作者:CodingRabbit
版权声明:本文首发于CodingRabbit的博客,转载请注明出处!