Java 注解的运行原理:从定义到运行时读取
文章摘要
Java 注解用于把元数据附加到类、方法、字段等程序结构上。本文以 JDK 8 为基础,沿着一条连续的示例说明注解成员如何声明和赋值、RetentionPolicy如何决定注解的保留范围、javac如何把注解写入 Class 文件,以及反射如何读取注解成员。最后再说明常见 JDK 8 实现如何用代理对象返回成员值,并明确这部分实现细节的适用边界。
版本说明:示例使用 JDK 8 语法。
AnnotationInvocationHandler是常见 OpenJDK 8 实现中的内部类,不是 Java 规范要求所有 JDK 都采用的实现方式。
一、先看注解从源码到读取的路径
理解注解的运行过程,只需要跟着这条路径走:
声明注解类型及成员 ↓ 在程序元素上使用注解并填写成员值 ↓ javac 根据保留策略将注解编码到 Class 文件 ↓ 反射读取运行时可见的注解 ↓ 通过注解对象的方法读取成员值后文会按这条路径逐步解释,从注解声明开始,不需要预先了解代理或 Class 文件内部结构。
二、声明和使用自定义注解
1.@interface声明注解类型
public@interfaceMyAnnotation{Stringname();intage();}@interface是 Java 声明注解类型的语法。name()和age()看起来像接口方法,但在注解类型中表示注解成员(也称元素),并不要求开发者提供方法实现。
注解类型与java.lang.annotation.Annotation有关。该接口提供了注解对象的通用能力;注解成员的声明、默认值和使用方式则由 Java 语言规则定义。
2. 在程序元素上使用注解
@MyAnnotation(name="Tom",age=20)publicclassUser{}name = "Tom"和age = 20是为注解成员指定值,不是调用name("Tom")这样的普通方法。成员名称必须与注解类型中声明的名称一致,类型也必须匹配。
3. 默认值与成员类型
注解成员可以声明默认值:
public@interfaceMyAnnotation{Stringname()default"unknown";intage()default18;}使用时可以省略有默认值的成员:
@MyAnnotationpublicclassUser{}读取时,未显式指定的成员使用声明的默认值。默认值保存在注解类型中,并在读取注解时应用;它不等同于每个使用位置都显式写入一份值。因此,修改默认值可能影响已编译、且没有显式指定该成员值的代码。
注解成员允许的类型包括基本类型、String、Class、枚举、注解类型,以及这些类型的一维数组。例如:
public@interfaceRoute{Stringpath();RequestMethodmethod()defaultRequestMethod.GET;Class<?>handler()defaultObject.class;String[]tags()default{};}三、保留策略决定注解能否在运行时读取
通过@Retention可以声明注解保留到哪个阶段:
@Retention(RetentionPolicy.RUNTIME)public@interfaceMyAnnotation{Stringname();intage();}三种策略的区别如下:
| 策略 | 保留范围 | 普通运行时反射能否读取 |
|---|---|---|
SOURCE | 只在源码处理阶段保留 | 不能 |
CLASS | 写入 Class 文件,运行时反射通常不可见 | 不能 |
RUNTIME | 写入 Class 文件并保留到运行时 | 可以 |
没有显式声明@Retention时,默认策略是CLASS。如果希望使用Class.getAnnotation()、Method.getAnnotation()等反射 API 读取注解,应声明RUNTIME。
@Target可以限制注解的使用位置。例如:
@Target(ElementType.TYPE)public@interfaceMyAnnotation{Stringname();}ElementType.TYPE表示该注解可用于类型声明。若未声明@Target,注解可以用于语言允许的程序元素位置。@Retention和@Target本身也是注解,通常称为元注解。
四、javac把注解信息写入 Class 文件
对于RUNTIME注解,编译器会把类型、成员名称和成员值等信息编码进 Class 文件的注解属性。它不是普通业务方法调用,也不是把一个运行时Map原样写进去。
常见属性名称包括:
RuntimeVisibleAnnotations:运行时可见的注解;RuntimeInvisibleAnnotations:写入 Class 文件、但运行时反射不可见的注解。因此,
CLASS不表示注解没有进入编译产物;字节码工具等仍可能读取它。它表示普通运行时反射不会把它作为运行时可见注解返回。可以用 JDK 自带的
javap查看编译结果:
javap-v-p-classpathout demo.User在输出中查找RuntimeVisibleAnnotations。Class 文件中的注解属性会通过常量池索引引用相应数据;常量池是 Class 文件内部保存类名、成员名、字面量等信息的结构。
五、反射如何读取注解
当注解带有RUNTIME保留策略时,可以通过反射读取:
MyAnnotationannotation=User.class.getAnnotation(MyAnnotation.class);if(annotation!=null){Stringname=annotation.name();intage=annotation.age();}可以把过程理解为:
User.class得到User对应的Class对象。getAnnotation()查找指定类型的运行时可见注解。JDK 解析 Class 文件中的注解属性,并应用注解成员的默认值。
反射 API 返回一个实现
MyAnnotation的注解对象。调用
annotation.name()或annotation.age()读取成员值。如果注解不存在、保留策略不是
RUNTIME,或注解不在当前查找规则覆盖的位置,getAnnotation()可能返回null。使用前应按 API 语义处理空值。到这里,开发者可见的使用方式已经完整:定义成员、在注解使用处给值、再通过反射访问值。下面的代理内容解释常见 JDK 8 实现内部如何提供这种访问方式。
六、常见 JDK 8 实现:代理对象返回注解成员值
1. 为什么会谈到动态代理
注解类型是特殊的接口类型。开发者通常不需要手写MyAnnotation的实现类。常见 OpenJDK 8 实现会用 JDK 动态代理创建注解对象,并把代理方法调用交给AnnotationInvocationHandler。这一节按User.class.getAnnotation(...)到annotation.name()的顺序拆开说明。
先看贯穿示例:
@MyAnnotation(name="Tom",age=20)publicclassUser{}MyAnnotationannotation=User.class.getAnnotation(MyAnnotation.class);Stringname=annotation.name();第一行的注解信息在编译时进入User.class;第二行请求反射 API 读取它;第三行通过注解成员方法取得name的值。后两步在常见 JDK 8 实现中的内部过程如下。
2. 第一步:解析注解属性并准备成员值
getAnnotation()找到User.class中与MyAnnotation对应的运行时注解信息后,JDK 8 的AnnotationParser会解析注解类型和成员值。解析时,它先用注解类型声明的默认值初始化成员值映射,再用 Class 文件中记录的显式值覆盖对应成员。
对于当前示例,供注解对象使用的数据可以简化表示为:
memberValues = { "name" → "Tom", "age" → 20 }如果注解使用处省略了有默认值的成员,该映射中相应值来自注解类型声明的默认值。若显式写了name = "Tom",解析出的显式值会覆盖默认值。
3. 第二步:为注解接口创建代理对象
OpenJDK 8 的AnnotationParser.annotationForMap(...)会调用Proxy.newProxyInstance(...),让生成的代理类实现MyAnnotation接口,并为代理关联一个AnnotationInvocationHandler。处理器持有注解类型和上一步准备好的成员值映射。
此时getAnnotation()返回的annotation在这个常见实现中就是代理对象。可以用下面的代码观察:
System.out.println(annotation.getClass().getName());System.out.println(Proxy.isProxyClass(annotation.getClass()));在常见 JDK 8 环境中,第一行可能输出类似com.sun.proxy.$Proxy...的类名,第二行通常输出true。代理类名称和编号会随环境变化,不能依赖它们编写业务逻辑。
4. 第三步:annotation.name()转交给处理器
调用:
Stringname=annotation.name();表面上是调用接口方法,实际过程可以概念化为:
调用代理对象的 name() ↓ 代理将调用转交给 InvocationHandler.invoke(proxy, method, args) ↓ AnnotationInvocationHandler 识别 method 对应 name() ↓ 从 memberValues 中读取 "name" 对应的值 ↓ 返回 "Tom",赋给 name 变量InvocationHandler.invoke收到三个参数:proxy是代理对象本身,method描述当前调用的方法,args是实参数组。注解成员是无参方法,所以这里的args为null。处理器通过method.getName()得到"name",再用这个名称查找成员值。对于int age(),处理器取回Integer值,代理方法再按方法声明的返回类型作为int返回。
因此,annotation.name()不是重新去User.class解析一次 Class 文件,也不是调用name("Tom")。元数据在反射读取时已经被解析并关联到注解对象,成员方法调用只是从该对象所持有的状态中取值。
5. 处理器还有哪些分支
注解代理调用并非所有方法都直接执行memberValues.get(methodName)。常见 OpenJDK 8 的处理器会先分流处理若干特殊方法:
annotationType()返回当前注解类型MyAnnotation.class;equals(Object)按注解规范比较注解类型及各成员值;hashCode()按注解成员计算符合注解约定的哈希值;toString()生成注解类型及成员值的字符串表示。其他无参注解成员方法才按成员名读取值。若成员值缺失,处理器会抛出
IncompleteAnnotationException;若 Class 文件中的成员值存在类型问题,处理器也可能抛出相应异常。对于数组成员,JDK 8 实现会返回数组副本,避免调用者修改返回数组时直接改动代理内部保存的值。
6. 这一实现的边界
以上调用链对应常见 OpenJDK 8 实现:AnnotationParser解析元数据、Proxy创建代理、AnnotationInvocationHandler分派调用。Java 规范保证注解和反射的语言/API 语义,但不要求所有 JDK 都用这些内部类、代理类名或相同数据结构。memberValues和AnnotationInvocationHandler都是实现细节,不是 Class 文件字段或供业务代码调用的公开 API。
七、最小完整示例与运行方式
下面把前文的片段合并成一个 JDK 8 示例。保存为demo/AnnotationDemo.java:
packagedemo;importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;importjava.lang.reflect.Proxy;@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)@interfaceMyAnnotation{Stringname()default"unknown";intage()default18;}@MyAnnotation(name="Tom",age=20)classUser{}publicclassAnnotationDemo{publicstaticvoidmain(String[]args){MyAnnotationannotation=User.class.getAnnotation(MyAnnotation.class);if(annotation==null){thrownewIllegalStateException("没有读取到运行时注解");}System.out.println("name = "+annotation.name());System.out.println("age = "+annotation.age());System.out.println("annotationType = "+annotation.annotationType().getName());System.out.println("proxyClass = "+annotation.getClass().getName());System.out.println("isJdkProxy = "+Proxy.isProxyClass(annotation.getClass()));}}在源码根目录编译并运行:
javac-dout demo/AnnotationDemo.javajava-cpout demo.AnnotationDemo javap-v-p-classpathout demo.User程序会输出Tom、20和注解类型名称。在常见 JDK 8 实现中,isJdkProxy会输出true;具体代理类名称不固定。javap输出中可以找到RuntimeVisibleAnnotations。
八、实现边界与常见误区
注解访问语法不代表普通方法实现
String name()在注解类型中声明的是注解成员。开发者通过annotation.name()读取它,但不需要手写实现类。
注解值不是直接存放在一个源码可见的 Map 中
源码中的注解先被编码进 Class 文件属性;运行时再由 JDK 解析。memberValues只是常见 JDK 实现中的运行时表示,不是 Class 文件本身。
CLASS注解不等于没有进入 Class 文件
CLASS注解可以出现在 Class 文件中,只是普通运行时反射不可见;字节码工具或其他构建阶段工具仍可能读取。
AnnotationInvocationHandler不是 Java 标准 API
它可以帮助理解常见 OpenJDK 8 的实现,但属于 JDK 内部实现细节。不同 JDK 版本或其他 JVM 可以采用不同内部结构;业务代码不应依赖该类。
注解规范保证语义,不限定代理实现
Java 规范定义注解类型、成员和反射行为,但不要求所有 JVM 都使用 JDK 动态代理或AnnotationInvocationHandler。因此应把“反射能按约定读取注解”作为通用结论,把代理实现限定在具体 JDK 实现范围内。
九、总结
Java 注解从源码到运行时读取的过程可以概括为:
@interface 声明成员 ↓ 使用注解并指定成员值 ↓ javac 按保留策略编码到 Class 文件 ↓ 反射读取运行时可见注解 ↓ 调用注解对象的方法取得成员值牢记三个阶段即可避免混淆:String name()是成员声明,name = "Tom"是赋值,annotation.name()是读取。常见 JDK 8 实现会用代理处理最后一步,但代理类和处理器属于实现细节。
注解运行流程图
参考资料
- Java Language Specification, Java SE 8:Annotation Type Elements
- Java Language Specification, Java SE 8:Annotations
- Java SE 8 API:
java.lang.annotation包 - Java SE 8 API:
Annotation接口 - Java SE 8 API:
Retention注解 - Java Virtual Machine Specification, Java SE 8:Class File Format
- Java Virtual Machine Specification, Java SE 8:RuntimeVisibleAnnotations
- OpenJDK 8u:
AnnotationInvocationHandler源码 - OpenJDK 8u:
AnnotationParser源码 - Java SE 8 API:
InvocationHandler