浅析 java 中的注解是如何实现的
大家在日常的工作中,应该已经用过注解(Annotation)了,那么你有没有思考过注解到底是如何实现的呢?本文对此进行(浅层次的)分析。
结论
- 每个注解都
extend了java.lang.annotation.Annotation - 每个注解都是
interface - 注解用到了动态代理,它所用到的
InvocationHandler的实现类是AnnotationInvocationHandler
我画了个简单的思维导图 ⬇️
mindmap
root)注解(
每个注解都是 interface
每个注解都 extend 了 java.lang.annotation.Annotation<br/>(⬆️ 它也是接口)
获取注解的数据时,会用到动态代理,<br/>对应的 InvocationHandler 是<br/> AnnotationInvocationHandler
正文
代码及准备工作
代码
我写了如下的代码 ⬇️ 来论证上面的三个结论,请将代码保存为 AnnotationStudy.java。
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@MyAnnotation(func1 = "actual value for func1()", func3 = "actual value for func3()")
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 \"func1()\" method is: " + myAnnotation.func1());
System.out.println("The return value of the \"func2()\" method is: " + myAnnotation.func2());
System.out.println("The return value of the \"func3()\" method is: " + myAnnotation.func3());
}
}
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation {
String func1() default "placeholder for func1()";
String func2() default "placeholder for func2()";
String func3();
}
MyAnnotation 注解中的 3 个方法
MyAnnotation 类中定义了如下的 3 个方法
| 方法 | 是否有默认值 | AnnotationStudy 上是否显式提供了对应的值 |
|---|---|---|
func1() | ✅ | ✅ |
func2() | ✅ | ❌ |
func3() | ❌ | ✅ |
javac AnnotationStudy.java 命令可以编译 AnnotationStudy.java。
编译后,会生成如下两个 class 文件
AnnotationStudy.classMyAnnotation.class
执行 java AnnotationStudy 命令后,会看到如下的结果
MyAnnotation is an interface: true
The name of myAnnotation's class is: $Proxy1
The return value of the "func1()" method is: actual value for func1()
The return value of the "func2()" method is: placeholder for func2()
The return value of the "func3()" method is: actual value for func3()
用 javap -v -p AnnotationStudy 命令可以查看 AnnotationStudy.class 的内容。
完整的结果有一百多行,这里就不展示了。 结果中有如下几行 ⬇️
RuntimeVisibleAnnotations:
0: #67(#50=s#68,#58=s#69)
MyAnnotation(
func1="actual value for func1()"
func3="actual value for func3()"
)
从这几行可以看出,我们代码中的第 4 行,即下面这行的信息,会保存在 AnnotationStudy.class 文件的 RuntimeVisibleAnnotations 属性中。
@MyAnnotation(func1 = "actual value for func1()", func3 = "actual value for func3()")
关于 RuntimeVisibleAnnotations 属性的结构,
Java Virtual Machine Specification 中的 4.7.16. The RuntimeVisibleAnnotations Attribute 小节 有相关介绍,这里就不展开了。其中提到了下面的内容 ⬇️
The
RuntimeVisibleAnnotationsattribute stores run-time visible annotations on the declaration of the corresponding class, field, method, or record component.
那么 MyAnnotation 注解中,func1() 和 func2() 的默认值保存在了哪里呢?
我们用 javap -v -p MyAnnotation 命令可以查看 MyAnnotation.class 的内容。
部分结果如下(常量池等部分略)⬇️
{
public abstract java.lang.String func1();
descriptor: ()Ljava/lang/String;
flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
AnnotationDefault:
default_value: s#10
"placeholder for func1()"
public abstract java.lang.String func2();
descriptor: ()Ljava/lang/String;
flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
AnnotationDefault:
default_value: s#12
"placeholder for func2()"
public abstract java.lang.String func3();
descriptor: ()Ljava/lang/String;
flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
}
SourceFile: "AnnotationStudy.java"
RuntimeVisibleAnnotations:
0: #17(#18=e#19.#20)
java.lang.annotation.Retention(
value=Ljava/lang/annotation/RetentionPolicy;.RUNTIME
)
从 javap 的处理结果可以看出,
func1()/func2()/func3()都是抽象(abstract)方法func1()/func2()的默认值保存在了AnnotationDefault属性中
Java Virtual Machine Specification 中的 4.7.22. The AnnotationDefault Attribute 中提到了 AnnotationDefault 属性的结构,这里不展开说。
其中提到了以下内容 ⬇️
The
AnnotationDefaultattribute records the default value (JLS §9.6.2) for the element represented by themethod_infostructure.
准备工作已经做好了,我们现在来验证前文提到的那 3 个结论。
结论 1: 每个注解都 extend 了 java.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
minor version: 0
major version: 66
flags: (0x2600) ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
这几行里的第一行是 ⬇️
interface MyAnnotation extends java.lang.annotation.Annotation
由此可见 MyAnnotation 确实 extend 了 java.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. To distinguish an annotation interface declaration from a normal interface declaration, the keyword
interfaceis preceded by an at sign (@).
由此可见,每个注解都是 interface。
除此之外,我们也可以从 class 文件的内容来着手。
在 javap -v -p MyAnnotation 的结果中,有如下的内容 ⬇️
interface MyAnnotation extends java.lang.annotation.Annotation
minor version: 0
major version: 66
flags: (0x2600) ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
这里的第 4 行有个 flags,这个 flags 是指什么呢?
Java Virtual Machine Specification 中的 4.1. The ClassFile Structure 小节 里提到了 class 文件的结构 ⬇️
我们刚刚说的 flags 其实就是 class 文件中的 access_flags。
flags 的值为 0x2600,而 0x2600 = 0x2000 + 0x0400 + 0x0200,所以一共有 3 个 flag 被置位。用大白话说,就是有 3 个 flag 的值不是 0。
这 3 个 flag 分别是 ⬇️
ACC_ANNOTATION:0x2000ACC_ABSTRACT:0x0400ACC_INTERFACE:0x0200
但是为什么这 3 个 flag 会被置位呢?
在 Java Virtual Machine Specification 中的 4.1. The ClassFile Structure 小节 里可以找到原因(重要的部分我用红线标出来了)⬇️
由此可见,对任意的注解而言,其 class 文件中的 ACC_ANNOTATION/ACC_ABSTRACT/ACC_INTERFACE 这 3 个 flag 都应该被置位。
所以 class 文件的内容也可以辅助证明所有注解都是 interface 这一论断。
结论 3: 注解用到了动态代理,它所用到的 InvocationHandler 的实现类是AnnotationInvocationHandler
在使用 MyBatis 时,可以定义 mapper interface。当我们借助 mapper interface 的实例来执行 SQL 语句时,这些实例自然是来自某个/某些实现类。与此类似,因为所有注解都是接口(interface),所以当我们获取到接口的某个实例时,这个实例必然属于某个实现类。
先提几个问题 ⬇️
- 注解背后的实现类是什么?
- 注解是否用到了动态代理?
- 如果注解用到了动态代理,那么注解所使用的
InvocationHandler的实现类是什么?
带着这几个问题,我们再看一下上文的代码的运行结果(下图是在 Intellij IDEA 中运行的结果,在命令行执行 java AnnotationStudy 也可以看到这样的输出)
从输出中可以看到,MyAnnotation 的实例 myAnnotation 的类型是 $Proxy1。奇怪,这是什么?我们并没有定义名叫 $Proxy1 的 class。我们在这里(也就是代码的第 9 行)打个断点,看看发生了什么。Debug 后,会看到 myAnnotation 这个实例中有个名为 h 的字段 ⬇️
借助 Intellij IDEA,可以看到 h 的精确类型是 sun.reflect.annotation.AnnotationInvocationHandler ⬇️
在 AnnotationInvocationHandler.java 中可以看到sun.reflect.annotation.AnnotationInvocationHandler 这个类的源码。
先画张简单的类图 ⬇️
注意:在上面的类图中,InvocationHandler 中的 invoke(...) 方法会 throws Throwable,但我不知道类图中应该如何展示 throws Throwable,所以就把它省略了。
从类图中可以看到,AnnotationInvocationHandler 类 implement 了 InvocationHandler 这个接口,而我们在使用 JDK 的动态代理时,需要提供 InvocationHandler 的实现类。
这样看来注解的实现用到了 JDK 的动态代理,而 AnnotationInvocationHandler 就是注解所用到的 InvocationHandler 的实现类。
不过到这里只是推测,还需要验证一下。
如果这里用到了动态代理的话,那么执行如下的命令将会生成代理类的 class 文件。
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.classAnnotationStudy.classMyAnnotation.class
和之前的结果相比,多了 $Proxy1.class。
我们用 javap -v -p '$Proxy1' 命令可以查看 $Proxy1.class 的内容。
完整的内容比较长,我们可以用 javap -p '$Proxy1' 查看简略的结果 ⬇️
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;
private static final java.lang.reflect.Method m5;
private static final java.lang.reflect.Method m6;
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 func1();
public final java.lang.String func2();
public final java.lang.String func3();
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 类中有 type 和 memberValues 字段
照理说,AnnotationInvocationHandler 的构造函数中应该会有些预处理的逻辑,而它的invoke(Object proxy, Method method, Object[] args) 方法中应该会有如何分派方法的逻辑(比如 MyAnnotation 中有 value() 方法,那么 value() 方法的返回值应该是在 invoke(...) 方法中处理好的)。于是我们就有了如下的任务列表 ⬇️
-
AnnotationInvocationHandler的构造函数 -
AnnotationInvocationHandler的invoke(Object proxy, Method method, Object[] args)方法
AnnotationInvocationHandler 的构造函数
我们可以在 AnnotationInvocationHandler 构造函数的入口处打个断点,然后 debug。
下图展示了断点的位置。
开始 debug 后,需要观察 type 的值,因为我们暂时只关心 MyAnnotation 的情形,所以当 type 是其他值时,可以继续。
下图展示了当 type 是 MyAnnotation 时,相关值的具体内容 ⬇️
从上图可见,当进入 AnnotationInvocationHandler 的构造函数时
- 参数
type是MyAnnotation对应的class对象 - 参数
memberValues是一个LinkedHashMap,其中存储了MyAnnotation注解中 方法名 -> 返回值 的映射关系,具体的映射关系如下表所示 ⬇️
| 方法名 | 返回值 |
|---|---|
func1 | "actual value for func1()" |
func2 | "placeholder for func2()" |
func3 | "actual value for func3()" |
构造函数中的逻辑比较直观,它会
- 进行基本的参数检查
- 用
type参数给this.type字段赋值 - 用
memberValues参数给this.memberValues字段赋值
这里有个问题,this.memberValues 中保存了 方法名 -> 返回值 的映射关系,那么如果注解中定义了重载的方法,例如下面这样 ⬇️ 那要怎么保存这种映射关系呢?
public @interface AFakeAnnotation {
String function1(int a);
String function1(int a, int b);
}
不必担心,因为上面的这个 AFakeAnnotation 是不合法的,编译时会有报错。
在注解中定义的方法,有不少限制,限制之一是这些方法不允许带有参数。既然注解中定义的方法都不能带有参数,那么在注解中也就不可能存在重载方法了。
Java Language Specification 中的 9.6.1. Annotation Interface Elements 小节 对注解中定义的方法的限制有如下的描述 ⬇️
By virtue of the grammar above, a method declaration in an annotation interface declaration cannot have formal parameters, type parameters, or a
throwsclause; and cannot beprivate,default, orstatic. Thus, an annotation interface cannot have the same variety of methods as a normal interface. Note that it is still possible for an annotation interface to inherit a default method from its implicit superinterface,java.lang.annotation.Annotation, though no such default method exists as of Java SE 24.
这些内容在下图蓝色方框的位置 ⬇️
虽然在注解中定义的方法都不能带有参数,但注解也是 Object,所以注解中会有来自 Object 类的 equals(Object) 方法。这样说来,我们还是可以写出下面这样的(古怪的)注解,而它的 equals() 方法和 Object 中的 equals(Object) 方法构成了重载的关系。
public @interface WeirdAnnotation {
boolean equals();
}
不过即使这样,this.memberValues 也还是能正常运转,下文马上会解释具体的原因。
小结
AnnotationInvocationHandler 的构造函数会
- 将注解对应的
class对象(例如MyAnnotation.class)保存在this.type字段中 - 将 方法名 -> 返回值 的映射关系保存在
this.memberValues字段中(这个字段是Map<String, Object>类型的
任务列表更新后如下 ⬇️
-
AnnotationInvocationHandler的构造函数 -
AnnotationInvocationHandler的invoke(Object proxy, Method method, Object[] args)方法
AnnotationInvocationHandler 的 invoke(Object proxy, Method method, Object[] args) 方法
我们可以在 invoke(...) 方法的入口处打个断点,然后 debug。
下图展示了断点的位置 ⬇️
开始 debug 后,需要观察 method 参数的值,因为我们暂时只关心 MyAnnotation 的情形,所以当 method 参数与 MyAnnotation 无关时,可以继续。
当执行到 MyAnnotation 中的 func1() 方法时,断点的具体情况如下图所示 ⬇️
我们来看看 invoke(...) 方法里大致做了些什么
- 用 方法名 给
member这个局部变量赋值 - 找到对应的返回值
- 如果
method参数对应Object中的equals(Object)方法,则进入方框1(见下图) - 如果
method参数对应Object中的toString()方法,则进入方框2(见下图) - 如果
method参数对应Object中的hashCode()方法,则进入方框3(见下图) - 如果
method参数对应Annotation中的annotationType()方法,则进入方框4(见下图),其实就是直接将this.type返回 - 如果上述的
i/ii/iii/iv都不成立,那么method就是这个注解中定义的方法,进入方框5(见下图),由于this.memberValues中保存了 方法名 -> 返回值 的映射关系,所以通过查询this.memberValues就知道对应的返回值是什么了(其实方框5之后还有点逻辑,这里略)。
- 如果
如果我们定义一个古怪的注解 ⬇️ 那么它的 equals() 方法会在上图中的方框 5 中处理(而不是在方框 1 中处理)。
public @interface WeirdAnnotation {
boolean equals();
}
一个例子
关于 invoke(...) 方法中的逻辑的解释,可能还是有点抽象,我把一段代码的时序图画出来,方便大家验证/思考。
我以 myAnnotation.func1() 为例,用时序图来分析背后的逻辑(myAnnotation.func1() 的位置如下图所示 ⬇️)
sequenceDiagram
participant m as main(...) method in AnnotationStudy
participant p as $Proxy1 instance
participant h as h
participant mv as memberValues
m ->> p: func1()
Note right of p: m3 is a method object for <br/>func1() method in MyAnnotation ⬇️
p ->> h: invoke(this, m3, null)
h ->> mv: get("func1")
Note right of mv: ⬅️ memberValues is a Map<String, Object> field in h
mv -->> h: return "actual value for func1()"
h -->> p: return "actual value for func1()"
p -->> m: return "actual value for func1()"
注意
- 这张时序图中只涉及
main(...)方法里的myAnnotation.func1() - 这张时序图中只画了各个方法的主要逻辑。参数检查,异常处理之类的逻辑都没画。
上方时序图的解释
m3是一个Method对象,它和MyAnnotation中的func1()方法对应h的精确类型是AnnotationInvocationHandler,所以h里有memberValues字段$Proxy1implement了MyAnnotation接口,所以可以通过$Proxy1的实例来调用func1()方法 (MyAnnotation中定义了func1()方法),Intellij IDEA中所展示的$Proxy1.class中的func1()方法是这样的 ⬇️
public final String func1() {
try {
return (String)super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
至于为何 func1() 会与 m3 对应,我们看一下完整的对应关系 ⬇️ (对理解注解的实现机制而言,这些对应关系的顺序并不重要)
代理类 $Proxy1 中的方法 | 代理类 $Proxy1 中,与之对应的 Method 字段是什么 |
|---|---|
hashCode() (来自 Object 类) | m0 |
equals(Object) (来自 Object 类) | m1 |
toString() (来自 Object 类) | m2 |
func1() | m3 |
func2() | m4 |
func3() | m5 |
annotationType() (来自 Annotation 类) | m6 |
小结
在 AnnotationInvocationHandler 的 invoke(Object proxy, Method method, Object[] args) 方法中,会
- 找到和
method参数对应的返回值- 如果
method和以下4个方法中的某一个对应,则进入相应的处理逻辑Object.equals(Object)Object.toString()Object.hashCode()Annotation.annotationType()
- 否则,用
this.memberValues查询和method对应的返回值
- 如果
任务列表更新后如下 ⬇️
-
AnnotationInvocationHandler的构造函数 -
AnnotationInvocationHandler的invoke(Object proxy, Method method, Object[] args)方法
正文完。