Java 5 引入注解机制(Annotation),提供了一种安全、类型检查的元数据机制,用于为代码提供额外信息,而不直接影响代码逻辑。注解本身不改变程序的行为,但可以通过反射、编译器插件或运行时框架(如 Spring、Hibernate)来读取并处理这些元数据,从而实现如依赖注入、ORM 映射、权限控制等功能。
本文将介绍解析 Java 注解的实现原理。
一、注解是什么
1. 注解是一种接口(interface)
在 Java 中,注解本质上是一个继承自 java.lang.annotation.Annotation 的特殊接口。
例如,定义一个注解:
public @interface MyAnnotation {
String value() default "default";
}
编译后,MyAnnotation.class 实际上是一个接口,继承自 java.lang.annotation.Annotation:
public interface MyAnnotation extends java.lang.annotation.Annotation {
String value();
}
✅ 所以你可以通过 MyAnnotation.class.isAnnotation() 判断它是否是注解类型,返回 true。
2. 注解的使用方式
注解可以加在类、方法、字段、参数等位置,例如:
@MyAnnotation("hello")
public class MyClass {}
此时,JVM 会在运行时(如果保留策略允许)将该注解信息存储在 Class 对象的元数据中。
二、注解的保留策略(RetentionPolicy)
注解的生命周期由 @Retention 决定,具体策略如下:
|
RetentionPolicy |
存在阶段 |
反射可读取 |
典型用途示例 |
|
SOURCE |
仅存在于源代码中,编译时丢弃 |
❌ 否 |
@Override @SuppressWarnings |
|
CLASS(默认) |
存在于 .class 字节码文件中,但 JVM 加载时不保留 |
❌ 否 |
字节码分析工具、编译期处理(如 Lombok 的部分注解) |
|
RUNTIME |
存在于 .class 文件中,且 JVM 运行时加载并保留 |
✅ 是 |
Spring 的 @Autowired、JUnit 的 @Test、自定义运行时注解 |
只有 RUNTIME 策略的注解才能通过 Class.getAnnotation() 等反射 API 在运行时获取。
三、注解底层存储与反射机制
1. 字节码中的注解存储
编译器将注解信息以 属性(Attribute) 的形式写入 class 文件的相应结构中:
- 类注解 → 写入 ClassFile 的 attributes 表
- 方法注解 → 写入 method_info 的 attributes 表
- 字段注解 → 写入 field_info 的 attributes 表
具体属性名包括:
- RuntimeVisibleAnnotations:对应 RetentionPolicy.RUNTIME
- RuntimeInvisibleAnnotations:对应 CLASS(JVM 不加载,但可被字节码工具读取)
- RuntimeVisibleParameterAnnotations:方法参数上的运行时可见注解
这些属性以 键值对 + 类型描述符 的形式存储,例如注解的类型、成员名、值等。
2. JVM 加载 Class 时的处理
当 JVM 加载一个类(如 MyClass.class)时,如果注解保留策略是 RUNTIME,会将注解信息解析并缓存到 java.lang.Class、java.lang.reflect.Method、Field 等反射对象中。
具体来说,在 OpenJDK 源码中(以 JDK 17 为例):
- 注解信息最终由 sun.reflect.annotation.AnnotationParser 解析。
- Class 类中通过 getAnnotations()、getDeclaredAnnotations() 等方法暴露注解。
3. 反射获取注解的源码路径
当你调用:
MyAnnotation annotation = MyClass.class.getAnnotation(MyAnnotation.class);
底层调用链大致如下:

四、注解实现关键源码解析
注解接口本身没有实现类,但 JVM 在运行时通过 动态代理(Dynamic Proxy) 为每个注解创建代理实例。
例如:
MyAnnotation annotation = MyClass.class.getAnnotation(MyAnnotation.class);
System.out.println(annotation.value()); // 调用代理对象的方法
这个 annotation 实际上是一个 Proxy 对象,其 InvocationHandler 是 AnnotationInvocationHandler(位于 sun.reflect.annotation 包)。
关键源码:
// AnnotationParser.java (OpenJDK)
private static Annotation parseAnnotation2(...) {
// 构建成员值映射:Map<String, Object> memberValues
return annotationForMap(annotationType, memberValues);
}
private static <A extends Annotation> A annotationForMap(
Class<A> type, Map<String, Object> memberValues) {
return (A) Proxy.newProxyInstance(
type.getClassLoader(),
new Class<?>[] { type },
new AnnotationInvocationHandler(type, memberValues)
);
}
- AnnotationInvocationHandler 实现了 invoke() 方法,当调用 annotation.value() 时,实际是从 memberValues 中取出 “value” 对应的值。
- ⚠️ 源码位于sun.*包是内部实现,不提议直接使用。
五、注解的成员值类型限制
注解的成员值只能是以下类型(JLS 规定):
- 基本类型(int, boolean 等)
- String
- Class
- 枚举(enum)
- 其他注解(嵌套)
- 以上类型的数组
这些限制确保注解值可以被序列化到 .class 文件中,并在运行时安全反序列化。
六、自定义注解并处理实战
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Loggable {
String value() default "";
}
public class Service {
@Loggable("user login")
public void login() {
System.out.println("login...");
}
}
// 使用反射读取
Method method = Service.class.getMethod("login");
if (method.isAnnotationPresent(Loggable.class)) {
Loggable log = method.getAnnotation(Loggable.class);
System.out.println("Log: " + log.value()); // 输出: Log: user login
}
底层就是通过上述的代理 + 解析机制实现的。
七、进阶编译期注解处理(APT)
在作者之前的文章中有介绍 APT机制,可以参考。
除了运行时反射,注解还可用于编译期处理(如 Lombok、Dagger),通过 Annotation Processing Tool (APT) 在编译时生成代码。
这属于 SOURCE 或 CLASS 策略的应用,不依赖反射,而是通过 javax.annotation.processing.Processor 实现。
八、注解使用提议
1. 避免在高频路径中重复反射读取注解
- 问题:Class.getAnnotation()、Method.getAnnotations() 等反射调用涉及解析、代理创建,首次调用较慢,频繁调用累积开销明显。
- 提议:优先使用 spring等框架提供的已支持缓存的AnnotationUtils工具类处理注解。
//1.使用 spring提供的AnnotationUtils
org.springframework.core.annotation.AnnotationUtils.findAnnotation(method, clazz);
//2.使用自定义缓存方式
private static final Map<Method, MyAnnotation> ANNOTATION_CACHE =
new ConcurrentHashMap<>();
public static MyAnnotation getAnnotation(Method method) {
return ANNOTATION_CACHE.computeIfAbsent(method,
m -> m.getAnnotation(MyAnnotation.class));
}
✅ Method、Field、Class 等反射对象是线程安全且可安全缓存的。
2. 使用 isAnnotationPresent()做存在性判断
- 缘由:isAnnotationPresent() 比 getAnnotation() 更轻量(无需构造代理对象)。
- 适用场景:只需判断是否有注解,不关心具体值。
if (method.isAnnotationPresent(Loggable.class)) {
// 再决定是否获取完整注解
Loggable ann = method.getAnnotation(Loggable.class);
}
3. 谨慎使用 RUNTIME保留策略
- 影响:RUNTIME 注解会被 JVM 加载到内存中,增加 Class 对象的元数据体积。
- 提议:
- 若仅用于编译期(如代码生成、静态检查),使用 SOURCE 或 CLASS。
- 避免在大量类/方法上无意义地标记 RUNTIME 注解。
4. 避免在注解中使用复杂或大对象
- 虽然注解值类型受限(不支持任意对象),但大数组或深层嵌套注解仍会:
- 增加 .class 文件体积
- 延长类加载和注解解析时间
- 提议:保持注解值简洁,必要时用字符串标识 + 外部配置映射。
5. 启动阶段完成注解扫描与处理
- 框架(如 Spring)一般在应用启动时一次性扫描并解析所有注解,构建元数据缓存。
- 自研框架提议:
- 避免在每次请求中扫描类路径或解析注解。
- 使用 BeanPostProcessor、ImportBeanDefinitionRegistrar 等机制在初始化阶段完成处理。
6. 注意注解代理对象的创建开销
- 每次 getAnnotation() 都会通过 Proxy.newProxyInstance() 创建代理实例(尽管内部有优化)。
- 缓存代理实例可避免重复创建(见第1条)。
7. JIT 优化友善性
- 反射调用(包括注解方法调用)不利于 JIT 内联优化。
- 若性能极其敏感(如高频交易系统),思考:
- 编译期代码生成(APT)替代运行时反射
- 使用字节码增强(如 ASM、ByteBuddy)直接植入逻辑
九、总结
|
层面 |
说明 |
|
语法层面 |
注解是继承 Annotation 的特殊接口 |
|
编译层面 |
javac将注解写入.class 文件的特定属性区 |
|
JVM 层面 |
加载类时解析注解(仅 RUNTIME 策略) |
|
运行时 |
通过反射 API 获取注解,返回动态代理实例 |
|
实现机制 |
AnnotationParser + AnnotationInvocationHandler |