Java注解原理

注解的使用场景可以分为两种:一)编译期间通过注解解析器处理;2)运行期间反射使用;

注解保留策略

  • RetentionPolicy.SOURCE : 编译期可见,但不会写入到.class文件;
  • RetentionPolicy#CLASS : 会写入到.class文件中,但是会被JVM忽略(这是默认策略);
  • RetentionPolicy#RUNTIME :注解会被写入到.class文件中,并且JVM运行期间也可见,可以通过反射放射获取到;

注解的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.CLASS)
public @interface MyClass {
String name();
}

@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRuntime1 {
int id();
}

@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRuntime2 {}

@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.SOURCE)
public @interface MySource {}

编译完成后,每个注解都会生成自己的.class文件,看一下MyClass这个注解的.class文件的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
$ javap -p -v me/demo/MyClass.class
Picked up JAVA_TOOL_OPTIONS: -Duser.language=en -Dfile.encoding=UTF-8
Classfile /F:/demo/JavaDemo/out/production/JavaDemo/me/demo/MyClass.class
Last modified Sep 21, 2023; size 472 bytes
MD5 checksum cb49d6b6a5ed7ab9c2b85d731996fb5f
Compiled from "MyClass.java"
public interface me.demo.MyClass extends java.lang.annotation.Annotation
minor version: 0
major version: 61
flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
this_class: #1 // me/demo/MyClass
super_class: #3 // java/lang/Object
interfaces: 1, fields: 0, methods: 1, attributes: 2
Constant pool:
#1 = Class #2 // me/demo/MyClass
#2 = Utf8 me/demo/MyClass
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Class #6 // java/lang/annotation/Annotation
#6 = Utf8 java/lang/annotation/Annotation
#7 = Utf8 name
#8 = Utf8 ()Ljava/lang/String;
#9 = Utf8 SourceFile
#10 = Utf8 MyClass.java
#11 = Utf8 RuntimeVisibleAnnotations
#12 = Utf8 Ljava/lang/annotation/Target;
#13 = Utf8 value
#14 = Utf8 Ljava/lang/annotation/ElementType;
#15 = Utf8 TYPE
#16 = Utf8 METHOD
#17 = Utf8 FIELD
#18 = Utf8 CONSTRUCTOR
#19 = Utf8 PARAMETER
#20 = Utf8 Ljava/lang/annotation/Retention;
#21 = Utf8 Ljava/lang/annotation/RetentionPolicy;
#22 = Utf8 CLASS
{
public abstract java.lang.String name();
descriptor: ()Ljava/lang/String;
flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
}
SourceFile: "MyClass.java"
RuntimeVisibleAnnotations:
0: #12(#13=[e#14.#15,e#14.#16,e#14.#17,e#14.#18,e#14.#19])
java.lang.annotation.Target(
value=[Ljava/lang/annotation/ElementType;.TYPE,Ljava/lang/annotation/ElementType;.METHOD,Ljava/lang/annotation/ElementType;.FIELD,Ljava/lang/annotation/ElementType;.CONSTRUCTOR,Ljava/lang/annotation/ElementType;.PARAMETER]
)
1: #20(#13=e#21.#22)
java.lang.annotation.Retention(
value=Ljava/lang/annotation/RetentionPolicy;.CLASS
)

从MyClass.class文件第7行代码public interface MyClass extends java.lang.annotation.Annotation可以看出来MyClass实际上是继承自Annotation的接口类。这一点从java.lang.Class#getAnnotation这个方法也可以看出来:

1
2
3
4
5
6
//java/lang/Class.java
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {
Objects.requireNonNull(annotationClass);

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

注解的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@MySource()
@MyClass(name = "my_class")
@MyRuntime1(id = 16)
@MyRuntime2
public class TestAnnotation {

@MyClass(name = "my class constructor")
@MyRuntime1(id = 0)
TestAnnotation() {
}

private final String mDefault = "Hello";

@MyClass(name = "my class field")
@MyRuntime1(id = 1)
private String mValue = mDefault;

@MyClass(name = "my class method")
public String getValue() {
return mValue;
}

@MyRuntime2
public void setValue(
@MyClass(name = "my class param") String value
) {
mValue = value;
}
}

编译完成后我们使用javap命令看一下TestAnnotation.class的格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
$ javap -p -v me/demo/TestAnnotation.class
Picked up JAVA_TOOL_OPTIONS: -Duser.language=en -Dfile.encoding=UTF-8
Classfile /F:/demo/JavaDemo/out/production/JavaDemo/me/demo/TestAnnotation.class
Last modified Sep 21, 2023; size 1062 bytes
MD5 checksum 8ae94cb155dd4d060085e7c1fef200c5
Compiled from "TestAnnotation.java"
public class me.demo.TestAnnotation
minor version: 0
major version: 61
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #10 // me/demo/TestAnnotation
super_class: #2 // java/lang/Object
interfaces: 0, fields: 2, methods: 3, attributes: 3
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = String #8 // Hello
#8 = Utf8 Hello
#9 = Fieldref #10.#11 // me/demo/TestAnnotation.mDefault:Ljava/lang/String;
#10 = Class #12 // me/demo/TestAnnotation
#11 = NameAndType #13:#14 // mDefault:Ljava/lang/String;
#12 = Utf8 me/demo/TestAnnotation
#13 = Utf8 mDefault
#14 = Utf8 Ljava/lang/String;
#15 = Fieldref #10.#16 // me/demo/TestAnnotation.mValue:Ljava/lang/String;
#16 = NameAndType #17:#14 // mValue:Ljava/lang/String;
#17 = Utf8 mValue
#18 = Utf8 ConstantValue
#19 = Utf8 RuntimeVisibleAnnotations
#20 = Utf8 Lme/demo/MyRuntime1;
#21 = Utf8 id
#22 = Integer 1
#23 = Utf8 RuntimeInvisibleAnnotations
#24 = Utf8 Lme/demo/MyClass;
#25 = Utf8 name
#26 = Utf8 my class field
#27 = Utf8 Code
#28 = Utf8 LineNumberTable
#29 = Utf8 LocalVariableTable
#30 = Utf8 this
#31 = Utf8 Lme/demo/TestAnnotation;
#32 = Integer 0
#33 = Utf8 my class constructor
#34 = Utf8 getValue
#35 = Utf8 ()Ljava/lang/String;
#36 = Utf8 my class method
#37 = Utf8 setValue
#38 = Utf8 (Ljava/lang/String;)V
#39 = Utf8 value
#40 = Utf8 Lme/demo/MyRuntime2;
#41 = Utf8 RuntimeInvisibleParameterAnnotations
#42 = Utf8 my class param
#43 = Utf8 SourceFile
#44 = Utf8 TestAnnotation.java
#45 = Integer 16
#46 = Utf8 my_class
{
private final java.lang.String mDefault;
descriptor: Ljava/lang/String;
flags: (0x0012) ACC_PRIVATE, ACC_FINAL
ConstantValue: String Hello

private java.lang.String mValue;
descriptor: Ljava/lang/String;
flags: (0x0002) ACC_PRIVATE
RuntimeVisibleAnnotations:
0: #20(#21=I#22)
me.demo.MyRuntime1(
id=1
)
RuntimeInvisibleAnnotations:
0: #24(#25=s#26)
me.demo.MyClass(
name="my class field"
)

me.demo.TestAnnotation();
descriptor: ()V
flags: (0x0000)
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #7 // String Hello
7: putfield #9 // Field mDefault:Ljava/lang/String;
10: aload_0
11: ldc #7 // String Hello
13: putfield #15 // Field mValue:Ljava/lang/String;
16: return
LineNumberTable:
line 11: 0
line 14: 4
line 16: 10
line 12: 16
LocalVariableTable:
Start Length Slot Name Signature
0 17 0 this Lme/demo/TestAnnotation;
RuntimeVisibleAnnotations:
0: #20(#21=I#32)
me.demo.MyRuntime1(
id=0
)
RuntimeInvisibleAnnotations:
0: #24(#25=s#33)
me.demo.MyClass(
name="my class constructor"
)

public java.lang.String getValue();
descriptor: ()Ljava/lang/String;
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #15 // Field mValue:Ljava/lang/String;
4: areturn
LineNumberTable:
line 22: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lme/demo/TestAnnotation;
RuntimeInvisibleAnnotations:
0: #24(#25=s#36)
me.demo.MyClass(
name="my class method"
)

public void setValue(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #15 // Field mValue:Ljava/lang/String;
5: return
LineNumberTable:
line 29: 0
line 30: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lme/demo/TestAnnotation;
0 6 1 value Ljava/lang/String;
RuntimeVisibleAnnotations:
0: #40()
me.demo.MyRuntime2
RuntimeInvisibleParameterAnnotations:
parameter 0:
0: #24(#25=s#42)
me.demo.MyClass(
name="my class param"
)
}
SourceFile: "TestAnnotation.java"
RuntimeVisibleAnnotations:
0: #20(#21=I#45)
me.demo.MyRuntime1(
id=16
)
1: #40()
me.demo.MyRuntime2
RuntimeInvisibleAnnotations:
0: #24(#25=s#46)
me.demo.MyClass(
name="my_class"
)
  • 保留策略是RetentionPolicy.CLASS的注解会保存在RuntimeInvisibleAnnotations列表中;
  • 保留策略是RetentionPolicy.RUNTIME的注解类会被放在RuntimeVisibleAnnotations列表中;
  • 保留策略是RetentionPolicy.SOURCE的注解不在TestAnnotation.class中;
  • 方法参数的注解则保存在RuntimeInvisibleParameterAnnotations列表中;

Runtime时注解类真身

1
2
3
4
5
6
public class Main {
public static void main(String[] args) {
Class<TestAnnotation> testCls = TestAnnotation.class;
System.out.println(testCls.getAnnotation(MyRuntime1.class).getClass());
}
}

日志输出结果是:class jdk.proxy2.$Proxy1,而不是MyRuntime1。这是为什么呢?实际上,当我们反射获取注解时,虚拟机会通过动态代理的方式生成了一个代理类返回。通过设置虚拟机参数-Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true,虚拟机会把生成的代理类保存为文件。

进入.class文件的生成目录,使用如下命令运行main函数:

1
java -Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true -Dfile.encoding=UTF-8 -classpath ./ Main

执行完上述命令后,在当前目录的./jdk/proxy2/目录下就可以找到$Proxy1.class文件。

另外一种简单的方式就是在java main函数最前面(只要在执行反射获取注解之前就行)添加如下代码:

1
System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");

IDEA运行后会在工程主目录下的jdk/proxy2/文件夹中保存动态代理类。通过IDEA打开文件后可以看到如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public final class $Proxy1 extends Proxy implements MyRuntime1 {
private static final Method m0;
private static final Method m1;
private static final Method m2;
private static final Method m3;
private static final Method m4;

public $Proxy1(InvocationHandler var1) {
super(var1);
}

public final int hashCode() {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final boolean equals(Object var1) {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final String toString() {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final int id() {
try {
return (Integer)super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final Class annotationType() {
try {
return (Class)super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

static {
try {
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("me.demo.MyRuntime1").getMethod("id");
m4 = Class.forName("me.demo.MyRuntime1").getMethod("annotationType");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}

private static MethodHandles.Lookup proxyClassLookup(MethodHandles.Lookup var0) throws IllegalAccessException {
if (var0.lookupClass() == Proxy.class && var0.hasFullPrivilegeAccess()) {
return MethodHandles.lookup();
} else {
throw new IllegalAccessException(var0.toString());
}
}
}

注解类反射源码分析

Class/Method/Field都有自己的反射获取注解的接口,如下:java.lang.Class#getAnnotation java.lang.reflect.Method#getAnnotation java.lang.reflect.Field#getAnnotation。注意:当通过java.lang.Class#getAnnotation来获取Class的注解时,只能获取Class本身的注解,而不能获取到Class中方法和成员变量的注解。反射获取注解实例代码:

1
2
3
testCls.getAnnotation(MyRuntime1.class);
testCls.getMethod("setValue", String.class).getAnnotation(MyRuntime1.class);
testCls.getField("mValue").getAnnotation(MyRuntime1.class);

当调用java.lang.Class#getAnnotation(Class<A> annotationClass)方法反射获取注解类时,会把这个类本身的注解全部进行解析,然后生成对应的动态代理类,注册到map列表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//java/lang/Class.java
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {
Objects.requireNonNull(annotationClass);

return (A) annotationData().annotations.get(annotationClass);
}
private AnnotationData annotationData() {
while (true) { // retry loop
AnnotationData annotationData = this.annotationData;
int classRedefinedCount = this.classRedefinedCount;
if (annotationData != null &&
annotationData.redefinedCount == classRedefinedCount) {
return annotationData;
}
// null or stale annotationData -> optimistically create new instance
AnnotationData newAnnotationData = createAnnotationData(classRedefinedCount);
// try to install it
if (Atomic.casAnnotationData(this, annotationData, newAnnotationData)) {
// successfully installed new AnnotationData
return newAnnotationData;
}
}
}

参考文章

JAVA 注解的基本原理