注解的使用场景可以分为两种:一)编译期间通过注解解析器处理;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
| 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
| public <A extends Annotation> A getAnnotation(Class<A> annotationClass) { Objects.requireNonNull(annotationClass);
return (A) annotationData().annotations.get(annotationClass); } private AnnotationData annotationData() { while (true) { AnnotationData annotationData = this.annotationData; int classRedefinedCount = this.classRedefinedCount; if (annotationData != null && annotationData.redefinedCount == classRedefinedCount) { return annotationData; } AnnotationData newAnnotationData = createAnnotationData(classRedefinedCount); if (Atomic.casAnnotationData(this, annotationData, newAnnotationData)) { return newAnnotationData; } } }
|
参考文章
JAVA 注解的基本原理