注解与反射:从元数据到运行时逻辑
更新时间:2026-09-20。注解本身只是「贴在代码上的标签」,不做任何事;真正让标签生效的是编译期的注解处理器或运行期的反射。Spring 的
@Autowired、JUnit 的@Test都建立在这套机制上。
一、元注解:描述注解的注解
java
@Target(ElementType.FIELD) // 能用在哪:TYPE/METHOD/FIELD/PARAMETER...
@Retention(RetentionPolicy.RUNTIME) // 生命周期,最关键
@Documented // 是否进 Javadoc
public @interface NotNull {
String message() default "不能为空";
int max() default 100;
}@Retention | 注解保留到 | 谁能读到 | 例子 |
|---|---|---|---|
| SOURCE | 源码 | 编译器/APT,进不了 class | @Override、Lombok |
| CLASS(默认) | class 文件 | 字节码工具,运行时反射读不到 | 插桩/混淆 |
| RUNTIME | class + 运行时 | 反射可读 | Spring/JUnit 自定义注解 |
想在运行时用反射读取,必须显式声明 @Retention(RUNTIME),否则 getAnnotation 返回 null。
二、用反射读取注解
java
class User {
@NotNull(message = "姓名必填")
private String name;
}
Field f = User.class.getDeclaredField("name");
NotNull ann = f.getAnnotation(NotNull.class);
if (ann != null) {
System.out.println(ann.message()); // 姓名必填
}
// 遍历一个类所有字段做校验
for (Field field : User.class.getDeclaredFields()) {
NotNull n = field.getAnnotation(NotNull.class);
if (n != null) {
field.setAccessible(true); // 放开 private 访问
if (field.get(userObj) == null) throw new IllegalArgumentException(n.message());
}
}Class/Field/Method/Constructor 是反射四大对象:clazz.getDeclaredFields() 拿所有本类字段(含 private),getFields() 只拿 public 及继承的;私有成员要先 setAccessible(true)。

三、编译期处理:APT 与 Spring 扫描
- 注解处理器(APT):
AbstractProcessor在编译期扫描 SOURCE/CLASS 注解并生成新代码,Lombok、MapStruct、Dagger 走这条,零运行时反射开销。 - 运行期扫描:Spring 启动时用反射遍历 Bean 的类/方法/字段,找到
@Component/@Autowired/@RequestMapping再创建对象、注入依赖、注册路由。理解注解+反射就理解了框架「为什么贴个标签就能用」。
坑
- 忘了 @Retention(RUNTIME):运行时 getAnnotation 永远是 null,自定义框架注解最常见的低级错误。
- 反射性能开销:
getDeclaredField、invoke要做安全检查且难被 JIT 内联,热点路径应缓存Method/Field对象、考虑MethodHandle或代码生成。 - setAccessible 在模块化受限:Java 9+ 对其他模块(如 JDK 内部类)反射会抛
InaccessibleObjectException,需要--add-opens,库应减少非标准内部 API 反射。 - 注解不支持 null 默认值:注解属性类型只能是基本类型、String、Class、枚举、注解及它们的数组,默认值不能为 null(用空串/特殊哨兵表达)。
- 改了字段名反射字符串没跟着改:反射用字符串绕过编译检查,重构后运行期才炸,能用函数式/方法引用就别硬编码名字。
总结
注解是贴在代码上的元数据,能否被读取取决于 @Retention:SOURCE 只给编译器和 APT(如 Lombok)用,RUNTIME 才会保留到运行时供反射 getAnnotation 读取;框架通过 Class/Field/Method 反射扫描注解完成校验、注入与路由,访问私有成员要 setAccessible、Java 9 模块化后还受 add-opens 限制;生产代码要缓存反射对象并警惕字符串绕过编译检查。 注解驱动的依赖注入在后端框架中大量使用,见 backend/。