[Java] 浅析注解是如何实现的

浅析 java 中的注解是如何实现的

大家在日常的工作中,应该已经用过注解(Annotation)了,那么你有没有思考过注解到底是如何实现的呢?本文对此进行(浅层次的)分析。

结论

  1. 每个注解都 extendjava.lang.annotation.Annotation
  2. 每个注解都是 interface
  3. 注解用到了动态代理,其所用到的 InvocationHandler 实现类是 AnnotationInvocationHandler

正文

代码及准备工作

我写了些代码(如下)用于论证上面的三个结论,请将下方的代码保存为 AnnotationStudy.java

java 复制代码
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@MyAnnotation("some arbitrary value")
public class AnnotationStudy {
    public static void main(String[] args) {
        MyAnnotation myAnnotation = AnnotationStudy.class.getAnnotation(MyAnnotation.class);
        System.out.println("MyAnnotation is an interface: " + MyAnnotation.class.isInterface());
        System.out.println("The name of myAnnotation's class is: " + myAnnotation.getClass().getName());
        System.out.println("The return value of the \"value\" method is: " + myAnnotation.value());
    }
}

@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation {
    String value() default "placeholder";
}

javac AnnotationStudy.java 命令可以编译 AnnotationStudy.java。 编译后,会生成如下两个 class 文件

  • AnnotationStudy.class
  • MyAnnotation.class

执行 java AnnotationStudy 命令后,会看到如下的结果

结论 1: 每个注解都 extendjava.lang.annotation.Annotation

java.lang.annotation.Annotation 是一个接口(interface), 从 Annotation.java 中可以看到 Annotation 接口的 javadoc ⬇️

其中提到了所有的注解都会 extend java.lang.annotation.Annotation 这一接口。

我们从 class 文件层面验证一下。 用 javap -v -p MyAnnotation 命令可以查看 MyAnnotation.class 的内容。 它的主要内容如下图所示 ⬇️ 其中比较重要的部分,我用红线标出来了。

上图中的第一行是

interface MyAnnotation extends java.lang.annotation.Annotation

由此可见 MyAnnotation 确实 extendjava.lang.annotation.Annotation。 我画了个简单的类图来表示两者的关系 ⬇️

结论 2: 每个注解都是 interface

Java Language Specification 中的 9.6. Annotation Interfaces 小节 的开头提到 ⬇️

An annotation interface declaration specifies an annotation interface, a specialized kind of interface.

由此可见,每个注解都是 interface

除此之外,我们也可以从 class 文件的内容来着手。 在 javap -v -p MyAnnotation 的结果的前几行中,有如下的内容

text 复制代码
flags: (0x2600) ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION

这里 flags 是指什么呢? Java Virtual Machine Specification 中的 4.1. The ClassFile Structure 小节 里提到了 class 文件的结构 ⬇️

我们刚刚说的 flags 其实就是 class 文件中的 access_flags

flags 的值为 0x2600,而 0x2600 = 0x2000 + 0x0400 + 0x0200,所以一共有 3flag 被置位。用大白话说,就是有 3flag 的值不是 0。 这 3 个 flag 分别是 ⬇️

  • ACC_ANNOTATION: 0x2000
  • ACC_ABSTRACT: 0x0400
  • ACC_INTERFACE: 0x0200

但是为什么这 3flag 会被置位呢? 在 Java Virtual Machine Specification 中的 4.1. The ClassFile Structure 小节 里可以找到原因(重要的部分我用红线标出来了)⬇️

由此可见,对任意的注解而言,其 class 文件中的 ACC_ANNOTATION/ACC_ABSTRACT/ACC_INTERFACE3flag 都应该被置位。 所以 class 文件的内容也可以辅助证明所有注解都是 interface 这一论断。

结论 3: 注解用到了动态代理,其所用到的 InvocationHandler 实现类是 AnnotationInvocationHandler

在使用 MyBatis 时,可以定义 mapper interface。当我们借助 mapper interface 的实例来执行 SQL 语句时,这些实例自然是来自某个/某些实现类。与此类似,因为所有注解都是接口(interface),所以当我们获取到接口的某个实例时,这个实例必然属于某个实现类。

注解背后的实现类是什么呢? 注解是否用到了动态代理呢? 如果是的话,注解所使用的 InvocationHandler 的实现类是什么呢?

带着这几个问题,我们再看一下上文的代码的运行结果(下图是在 Intellij IDEA 中运行的结果,在命令行执行 java AnnotationStudy 也可以看到这样的输出)

从输出中可以看到,MyAnnotation 的实例 myAnnotation 的类型是 $Proxy1。好奇怪,这是什么?我们并没有定义名叫 $Proxy1class。我们在这里(也就是代码的第 9 行)打个断点,看看发生了什么。Debug 后,会看到 myAnnotation 这个实例中有个名为 h 的字段 ⬇️

借助 Intellij IDEA,可以看到 h 的精确类型是 sun.reflect.annotation.AnnotationInvocationHandler ⬇️

AnnotationInvocationHandler.java 中可以看到sun.reflect.annotation.AnnotationInvocationHandler 这个类的源码。

先画张简单的类图 ⬇️

注意:在上面的类图中,InvocationHandler 中的 invoke(...) 方法会 throws Throwable,但我不知道类图中应该如何展示 throws Throwable,所以就把它省略了。

从类图中可以看到,AnnotationInvocationHandlerimplementInvocationHandler 这个接口,而我们在使用 JDK 的动态代理时,需要提供 InvocationHandler 的实现类。 这样看来注解的实现用到了 JDK 的动态代理,而 AnnotationInvocationHandler 就是注解所用到的 InvocationHandler 的实现类。

不过到这里只是推测,还需要验证一下。

如果这里用到了动态代理的话,那么执行如下的命令将会生成代理类的 class 文件。

bash 复制代码
java -Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true AnnotationStudy

至于为什么要将 jdk.proxy.ProxyGenerator.saveGeneratedFiles 的值置为 true,可以参考 ProxyGenerator.java 中的 第 106 行第 213 行 以及相关的逻辑,这里就不展开说了。另外如果读者朋友对 JDK 的动态代理不太熟悉的话,可以另找文章看一看,本文不讨论其中的细节。

执行 java -Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true AnnotationStudy 命令后,会看到以下的 class 文件(另外还会生成一个名为 jdk 的目录,这里忽略)

  • $Proxy1.class
  • AnnotationStudy.class
  • MyAnnotation.class

和之前的结果相比,多了 $Proxy1.class

我们用 javap -v -p '$Proxy1' 命令可以查看 $Proxy1.class 的内容。 完整的内容比较长,如果只看简略内容的话,可以改为 javap -p '$Proxy1',这个命令的结果如下 ⬇️

text 复制代码
final class $Proxy1 extends java.lang.reflect.Proxy implements MyAnnotation {
  private static final java.lang.reflect.Method m0;
  private static final java.lang.reflect.Method m1;
  private static final java.lang.reflect.Method m2;
  private static final java.lang.reflect.Method m3;
  private static final java.lang.reflect.Method m4;
  public $Proxy1(java.lang.reflect.InvocationHandler);
  public final int hashCode();
  public final boolean equals(java.lang.Object);
  public final java.lang.String toString();
  public final java.lang.String value();
  public final java.lang.Class annotationType();
  static {};
  private static java.lang.invoke.MethodHandles$Lookup proxyClassLookup(java.lang.invoke.MethodHandles$Lookup) throws java.lang.IllegalAccessException;
}

利用 javap 命令提供的结果,可以画出如下的类图 ⬇️ (类图中省略了一些字段/方法)

注意:就本文的例子而言,上图中 java.lang.reflect.Proxy 类里的 h 字段的精确类型是 sun.reflect.annotation.AnnotationInvocationHandler。但在其他使用的动态代理的场景中,h 的精确类型可能会是其他的实现类。

在上方的类图中,可以看到 AnnotationInvocationHandler 类中有 typememberValues 字段

照理说,AnnotationInvocationHandler 的构造函数中应该会有些预处理的逻辑,而它的invoke(Object proxy, Method method, Object[] args) 方法中应该会有如何分派方法的逻辑(比如 MyAnnotation 中有 value() 方法,那么 value() 方法的返回值应该是在 invoke(...) 方法中处理好的)。于是我们就有了如下的任务列表 ⬇️

  • AnnotationInvocationHandler 的构造函数
  • AnnotationInvocationHandlerinvoke(Object proxy, Method method, Object[] args) 方法
AnnotationInvocationHandler 的构造函数

我们可以在 AnnotationInvocationHandler 构造函数的入口处打个断点,然后 debug。 下图展示了断点的位置。

开始 debug 后,需要观察 type 的值,因为我们暂时只关心 MyAnnotation 的情形,所以当 type 是其他值时,可以继续。 下图展示了当 typeMyAnnotation 时,相关值的具体内容 ⬇️

从上图可见,当进入 AnnotationInvocationHandler 的构造函数时

  • 参数 typeMyAnnotation 对应的 class 对象
  • 参数 memberValues 是一个 LinkedHashMap,其中存储了 MyAnnotation 注解中方法名到返回值的映射关系

构造函数中的逻辑比较直观,它会

  • 进行基本的参数检查
  • type 参数给 this.type 字段赋值
  • memberValues 参数给 this.memberValues 字段赋值
小结

AnnotationInvocationHandler 的构造函数,会

  • 将注解对应的 class 对象(例如 MyAnnotation.class)保存在 this.type 字段中
  • 方法名 -> 返回值 的映射关系保存在 this.memberValues 字段中(这个字段是 Map<String, Object> 类型的,例如 "value" -> "some arbitrary value" 可以是这个 map 中的一个 entry

任务列表更新后如下 ⬇️

  • AnnotationInvocationHandler 的构造函数
  • AnnotationInvocationHandlerinvoke(Object proxy, Method method, Object[] args) 方法
AnnotationInvocationHandlerinvoke(Object proxy, Method method, Object[] args) 方法

我们可以在 invoke(...) 方法的入口处打个断点,然后 debug。 下图展示了断点的位置 ⬇️

开始 debug 后,需要观察 method 参数的值,因为我们暂时只关心 MyAnnotation 的情形,所以当 method 参数与 MyAnnotation 无关时,可以继续。 当执行到 MyAnnotation 中的 value() 方法时,断点的具体情况如下图所示 ⬇️

我们来看看 invoke(...) 方法里大致做了些什么

  1. 方法名member 这个局部变量赋值
  2. 找到对应的返回值
    1. 如果 method 参数对应 Object 中的 equals(Object) 方法,则进入方框 1 (见下图)
    2. 如果 method 参数对应 Object 中的 toString() 方法,则进入方框 2 (见下图)
    3. 如果 method 参数对应 Object 中的 hashCode() 方法,则进入方框 3 (见下图)
    4. 如果 method 参数对应 Annotation 中的 annotationType() 方法,则进入方框 4 (见下图),其实就是直接将 this.type 返回
    5. 如果上述的 i/ii/iii/iv都不成立,那么 method 就是这个注解中定义的方法,进入方框 5(见下图),由于 this.memberValues 中保存了 方法名 -> 返回值 的映射关系,所以通过查询 this.memberValues 就知道对应的返回值是什么了(其实方框 5 之后还有点逻辑,这里略)
一个例子

这样说,可能还是有点抽象,我把一段代码的时序图画出来,方便大家验证/思考。 我以 myAnnotation.value() 为例,用时序图来分析背后的逻辑(myAnnotation.value() 的位置如下图所示 ⬇️)

sequenceDiagram participant m as main(...) method in AnnotationStudy participant p as $Proxy1 instance Note right of p: m3 reprents a method object for
value() method in MyAnnotation ⬇️ participant h as h Note right of h: ⬆️ h is an AnnotationInvocationHandler instance participant mv as memberValues Note right of mv: memberValues is a map field within h m ->> p: value() p ->> h: invoke(this, m3, null) h ->> mv: get("value") mv -->> h: return "some arbitrary value" h -->> p: return "some arbitrary value" p -->> m: return "some arbitrary value"

注意

  1. 这张时序图中只涉及 main(...) 方法里的 myAnnotation.value()
  2. 这张时序图中只画了各个方法的主要逻辑。参数检查,异常处理之类的逻辑都没画。

上方时序图的解释

  • m3 是一个 Method 对象,它和 MyAnnotation 中的 value() 方法对应
  • h 的精确类型是 AnnotationInvocationHandler,所以 h 里有 memberValues 字段
  • $Proxy1 implementMyAnnotation 接口,所以可以通过 $Proxy1 的实例来调用 value() 方法 (MyAnnotation 中定义了 value() 方法),Intellij IDEA 中所展示的 $Proxy1.class 中的 value() 方法的逻辑如下 ⬇️
小结

AnnotationInvocationHandlerinvoke(Object proxy, Method method, Object[] args) 方法中,会

  • 找到和 method 参数对应的返回值
    • 如果 method 和以下 4 个方法中的某一个对应,则进入相应的处理逻辑
      1. Object.equals(Object)
      2. Object.toString()
      3. Object.hashCode()
      4. Annotation.annotationType()
    • 否则,用 this.memberValues 查询和 method 对应的返回值

任务列表更新后如下 ⬇️

  • AnnotationInvocationHandler 的构造函数
  • AnnotationInvocationHandlerinvoke(Object proxy, Method method, Object[] args) 方法

参考资料

相关推荐
回家路上绕了弯1 小时前
ClickHouse 深度解析:从核心特性到实战应用,解锁 OLAP 领域新势能
数据库·后端
xiaok1 小时前
本地用VScode的Live Server监听5500访问页面,ubuntu上不需要在配置5500
后端
雨绸缪1 小时前
ABAP 时间戳
后端
m0_480502641 小时前
Rust 登堂 之 函数式编程(三)
开发语言·后端·rust
艾醒2 小时前
大模型面试题剖析:大模型微调与训练硬件成本计算
人工智能·后端·算法
自由生长20242 小时前
每日知识-设计模式-状态机模式
后端
用户298698530142 小时前
如何使用 Spire.Doc 在 C# 中创建、写入和读取 Word 文档?
后端
林太白2 小时前
项目中的层级模块到底如何做接口
前端·后端·node.js
一枚小小程序员哈2 小时前
基于Android的车位预售预租APP/基于Android的车位租赁系统APP/基于Android的车位管理系统APP
android·spring boot·后端·struts·spring·java-ee·maven
二闹3 小时前
从@Transactional失效场景到传播行为原理
java·后端