news 2026/9/25 21:16:07

【八八股股 | 第二篇】Java注解原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【八八股股 | 第二篇】Java注解原理

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();}

可以把过程理解为:

  1. User.class得到User对应的Class对象。

  2. getAnnotation()查找指定类型的运行时可见注解。

  3. JDK 解析 Class 文件中的注解属性,并应用注解成员的默认值。

  4. 反射 API 返回一个实现MyAnnotation的注解对象。

  5. 调用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 实现会用代理处理最后一步,但代理类和处理器属于实现细节。

注解运行流程图

参考资料

  1. Java Language Specification, Java SE 8:Annotation Type Elements
  2. Java Language Specification, Java SE 8:Annotations
  3. Java SE 8 API:java.lang.annotation包
  4. Java SE 8 API:Annotation接口
  5. Java SE 8 API:Retention注解
  6. Java Virtual Machine Specification, Java SE 8:Class File Format
  7. Java Virtual Machine Specification, Java SE 8:RuntimeVisibleAnnotations
  8. OpenJDK 8u:AnnotationInvocationHandler源码
  9. OpenJDK 8u:AnnotationParser源码
  10. Java SE 8 API:InvocationHandler
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 21:08:38

LEO空间环境模拟:航天材料如何实现真实与地面测试

航天器进入低地球轨道&#xff08;LEO&#xff09;后&#xff0c;表面材料和元器件处在 LEO空间环境里&#xff0c;真空、温度交变、原子氧AO、高能电子辐照和 VUV 往往同时叠加。真实空间暴露以年计。航天材料空间环境模拟的价值&#xff0c;是让航天材料测试先用空间环境模拟…

作者头像 李华
网站建设 2026/9/25 21:04:57

DeskcommCRM实战指南:从坐席工作台到数据驱动的客户管理

1. 它和普通 CRM 的差别&#xff0c;藏在“Deskcomm”这个名字里第一次看到“DeskcommCRM”这个名字的时候&#xff0c;我脑子里跳出来的其实是三个词&#xff1a;Desk、Comm、CRM。很多人把这类系统简单归类为“又一个客户管理软件”&#xff0c;但这个名字本身已经点明了它的…

作者头像 李华
网站建设 2026/9/25 20:59:51

G.8273.2边界时钟测试:纳秒级时频同步的生存能力验证

1. 这不是“测个时间”那么简单&#xff1a;G.8273.2边界时钟测试到底在测什么&#xff1f;ITU-T G.8273.2&#xff0c;这个编号看起来像一串密码&#xff0c;但对通信、电力、金融、广电这些对时间精度有“强迫症”的行业来说&#xff0c;它就是一把标尺&#xff0c;一把量度整…

作者头像 李华
网站建设 2026/9/25 20:56:51

Qt 离线语音识别:FFmpeg + sherpa-onnx(SenseVoice)视频转文字工具

本文内容由 AI 辅助生成&#xff0c;仅供学习参考。 项目&#xff1a;Qt5.14.2 MinGW64 FFmpeg sherpa-onnx-offline&#xff08;SenseVoice 离线语音识别&#xff09; 方案&#xff1a;QProcess 调用外部 exe&#xff0c;不编译源码 项目整体架构运行依赖清单UI 控件清单&am…

作者头像 李华