Java 注解完整总结:本质、定义、保留级别与生效机制
从注解的本质出发,理解 @Target、@Retention、注解处理器,以及 SOURCE、CLASS、RUNTIME 的区别,并厘清 Lombok 与 Spring 的真实工作方式。
Java 注解完整总结:本质、定义、保留级别与生效机制
在 Spring Boot 项目里,我们每天都在使用注解:@Autowired、@Transactional、@RequestMapping、@Override、@Deprecated,以及自己定义的业务注解。
注解看起来像是一种“特殊语法”,但它真正的价值不在于自己执行逻辑,而在于告诉编译器、工具或框架:这里有一条额外的信息,请按照规则处理它。
这篇文章从“注解到底是什么”开始,一步一步理解它如何定义、如何被读取,以及 SOURCE、CLASS、RUNTIME 为什么存在。
一、注解到底是什么?
注解本质上是一种特殊的接口声明。定义注解时使用 @interface:
public @interface MyAnnotation {
String value() default "";
}
从 Java 类型体系看,注解类型也属于接口的一种特殊形式,并且会继承 java.lang.annotation.Annotation 这一标记接口。可以把它理解成:
public interface MyAnnotation extends Annotation {
String value();
}
上面是帮助理解的等价示意,并不是让我们手动改写注解定义。
注解本身不包含“看到它就自动执行”的业务逻辑。它更像快递单上的贴纸:
- “易碎品”贴纸本身不会搬运快递。
- 快递员读取贴纸后,才会采取轻拿轻放的处理方式。
- 如果没有快递员或处理规则,贴纸就只是贴纸。
在 Java 中,编译器、注解处理器、反射代码和 Spring 等框架,就是负责读取这些“贴纸”的处理者。
二、如何定义一个注解?
一个可被 Spring AOP 在运行时读取的注解,通常需要关注三个部分。
1. @Target:这个注解可以贴在哪里?
@Target({ElementType.METHOD, ElementType.TYPE})
常见取值如下:
| 取值 | 作用位置 |
|---|---|
| ElementType.METHOD | 方法 |
| ElementType.TYPE | 类、接口、枚举或注解类型 |
| ElementType.FIELD | 字段 |
| ElementType.PARAMETER | 方法参数 |
| ElementType.CONSTRUCTOR | 构造方法 |
| ElementType.LOCAL_VARIABLE | 局部变量 |
| ElementType.PACKAGE | 包 |
| ElementType.ANNOTATION_TYPE | 其他注解类型 |
@Target 主要解决“能不能贴”的问题。如果目标位置不在允许范围内,编译器就会报错。
2. @Retention:这个注解保留多久?
@Retention(RetentionPolicy.RUNTIME)
RetentionPolicy 决定注解信息在编译和运行过程中的保留范围:
| 取值 | 保留范围 | 运行时反射能否读取 |
|---|---|---|
| SOURCE | 只保留在源码中,编译后不进入 class 文件 | 不能 |
| CLASS | 写入 class 文件,但 JVM 运行时通常不提供给反射 | 不能直接读取 |
| RUNTIME | 写入 class 文件,并在运行时保留 | 可以 |
3. 注解属性:需要记录什么信息?
public @interface MyLog {
String value() default "";
int level() default 1;
}
注解里的 value()、level() 看起来像方法,但它们表示的是注解属性。使用时可以这样写:
@MyLog(value = "测试", level = 2)
public void test() {
}
如果注解只有一个名为 value 的属性,使用时可以省略属性名:
@MyLog("测试")
public void test() {
}
三、注解为什么不会自动生效?
注解只是数据或标记,不是业务代码。它要发挥作用,必须有对应的处理机制。
常见的两种方式是:
| 处理方式 | 触发时机 | 典型例子 |
|---|---|---|
| 编译期处理 | 编译 Java 源码时 | Lombok、MapStruct 等注解处理器 |
| 运行期处理 | 程序启动或运行时 | Spring 反射扫描、Spring AOP |
工作流程可以概括为:
定义注解
↓
在类、方法或字段上贴注解
↓
处理器扫描并读取注解
↓
根据属性值执行检查、生成代码或改变运行行为
没有处理器时,自定义注解不会自动打印日志、校验权限或开启事务。
四、SOURCE、CLASS、RUNTIME 到底有什么区别?
1. SOURCE:只服务于源码和编译检查
SOURCE 注解只在源码阶段存在,编译后不会进入 class 文件。例如 @Override 的主要作用是告诉编译器检查当前方法是否真的重写了父类或接口方法。
@Override
public String toString() {
return "User";
}
如果方法名或参数写错,编译器会及时提示。检查完成后,运行时并不需要知道“这个方法当时是否写过 @Override”,所以没有必要保留。
@SuppressWarnings 也是类似思路:它影响编译器对警告的处理,运行时不需要依赖这个信息。
需要特别纠正一个容易记错的地方:@Deprecated 不是 SOURCE 级别,而是 RUNTIME。它不仅能让编译器和 IDE 提示 API 已过时,也可以在运行时通过反射读取。
2. CLASS:写入 class,但通常不供运行时反射使用
CLASS 表示注解信息会写入 .class 文件,但 JVM 加载后通常不把它作为运行时反射信息暴露出来。
这种级别适合某些只需要检查 class 文件、但不需要在程序运行时反射读取的工具场景。它是 Java 注解的默认保留级别:如果没有显式写 @Retention,默认就是 CLASS。
这里也要纠正一个常见说法:不能简单地说“Lombok 的 @Data 必须是 CLASS”。Lombok 的常见注解保留策略是 SOURCE,但 Lombok 可以在 Java 编译阶段通过注解处理和编译器扩展读取源码中的注解,并直接修改语法树、生成 getter、setter、构造方法等代码。
因此,注解处理器读取源码注解,并不等于运行时反射读取 class 文件中的注解。二者是不同阶段的读取机制。
3. RUNTIME:运行时仍然保留
如果 Spring、AOP 或自己的反射代码需要在程序运行过程中读取注解,就必须使用 RUNTIME:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLog {
String value() default "";
}
常见的运行时注解包括 @Autowired、@Transactional、@RequestMapping,以及通过反射或 AOP 读取的自定义注解。
五、三个例子应该怎样比较?
| 注解 | 主要读取者 | 主要作用 | 关键阶段 |
|---|---|---|---|
| @Override | Java 编译器 | 检查是否正确重写 | 编译期 |
| @Data | Lombok 编译期处理机制 | 修改编译树并生成成员方法 | 编译期 |
| @MyLog | Spring AOP 或反射处理器 | 触发日志、权限等运行逻辑 | 运行期 |
真正决定 Retention 的不是“这个注解有多重要”,而是“谁在什么阶段读取它”。
- 编译器用完就够了:通常选择 SOURCE。
- 工具需要读取 class 文件但运行时不需要:可以选择 CLASS。
- 程序运行时需要反射或 AOP 读取:必须选择 RUNTIME。
六、注解值能不能在方法内部直接使用?
不能。注解贴在方法声明上,不会自动变成方法内部的局部变量:
@MyLog(value = "测试")
public void test() {
// value 不是这里的局部变量,不能直接使用
}
如果想取得注解的属性值,必须由方法外部的反射代码或 AOP 处理器读取。
方式一:反射读取
Method method = Test.class.getMethod("test");
MyLog myLog = method.getAnnotation(MyLog.class);
String value = myLog.value();
如果目标方法没有注解,getAnnotation 可能返回 null,因此实际代码还需要进行空值判断。
方式二:AOP 读取
@Around("@annotation(myLog)")
public Object handle(
ProceedingJoinPoint pjp,
MyLog myLog) throws Throwable {
String value = myLog.value();
return pjp.proceed();
}
这里的 myLog 是 Spring AOP 根据切点绑定进来的注解对象,不是方法内部凭空出现的变量。
前提是注解必须使用 @Retention(RetentionPolicy.RUNTIME),否则运行时反射和 AOP 无法读取它。
七、为什么注解处理阶段和运行时反射不能混为一谈?
这是理解 SOURCE、CLASS、RUNTIME 的关键。
编译期注解处理
读取 Java 源码
↓
注解处理器参与编译
↓
读取注解并生成或修改代码
↓
得到 class 文件
↓
程序运行
Lombok、MapStruct 等工具主要在程序运行前完成工作。代码已经生成后,运行时不一定还需要保留注解。
运行期反射和 AOP
编译得到 class 文件
↓
JVM 加载 class
↓
Spring/AOP 在运行时扫描和读取注解
↓
根据注解信息创建代理、注册路由或执行增强逻辑
因此,“编译期处理器能读取”与“运行时反射能读取”是两个不同问题。不能因为某个工具能在编译期读取注解,就推断运行时也能通过反射读取。
八、如何选择保留级别?
注解只给编译器或 IDE 做检查?
└─ 通常选择 SOURCE
工具需要读取 class 文件,但运行时不需要反射?
└─ 可以选择 CLASS
Spring、AOP 或反射需要运行时读取?
└─ 必须选择 RUNTIME
不要简单因为“RUNTIME 最保险”就所有注解都使用 RUNTIME。保留级别应该与使用阶段匹配。只有确实需要运行时读取时,才选择 RUNTIME。
九、常见踩坑点
1. AOP 读不到自定义注解
通常是忘记写 @Retention(RetentionPolicy.RUNTIME),或者切点表达式、代理调用方式不正确。
2. 把注解当成变量
注解属性只能通过注解对象读取。方法内部不能直接使用声明处的 value 和 level。
3. 以为注解自带逻辑
注解只是标记。真正执行逻辑的是编译器、注解处理器、反射代码、AOP 切面或其他框架组件。
4. 认为 CLASS 一定能被反射读取
CLASS 只是写入 class 文件,不代表运行时反射可见。需要反射读取时,应明确使用 RUNTIME。
5. 把 Lombok 的工作方式理解成运行时魔法
Lombok 的 getter、setter 等方法是在编译期间生成或加入编译树的,不是程序启动后动态生成的普通业务逻辑。
十、三句话终极记忆
- @Override:告诉编译器检查重写是否正确,检查完成后通常不需要运行时保留。
- @Data:告诉 Lombok 在编译期生成代码,重点是编译期处理,不是运行时反射。
- @MyLog:告诉 Spring AOP 在运行时读取并执行增强逻辑,因此必须保留到 RUNTIME。
最后记住:
注解是标记,处理器才是执行者;Retention 决定处理器在什么阶段还能看见这个标记。