注解原理与编译期处理
# 21.注解原理与编译期处理
# 目录介绍
- 1. 案例引入
- 2. 注解的本质
- 3. 元注解全解
- 4. 编译期处理 APT
- 5. Lombok 字节码魔法
- 6. 运行时反射读取
- 7. Spring 注解体系
- 8. 自定义注解实战
- 9. 实战陷阱清单
- 10. 综合案例串讲
# 1. 案例引入
# 1.1 @Override 失效之谜
某团队在重构一个接口实现类时,把父类方法名从 getUser 改成了 findUser——IDE 没有报错,编译通过,测试也绿了。但上线后,所有调用都走了父类的默认实现,子类的重写逻辑完全没执行:
// 父类(重构后)
public abstract class BaseService {
public User findUser(Long id) {
return defaultUser(); // 默认实现
}
}
// 子类(忘记同步改名)
public class UserService extends BaseService {
@Override
public User getUser(Long id) { // ★ 方法名没改!
return userRepository.findById(id);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
疑惑:@Override 不是应该在编译期报错吗?为什么没有?
真相:@Override 的行为取决于编译器版本和目标类型——在 JDK 5 中,@Override 只检查父类方法,不检查接口方法;JDK 6 才修复。更关键的是——这个团队用的是 JDK 5 编译,父类是抽象类,但方法名改了之后子类的 getUser 变成了一个全新方法,而不是重写。
@Override 的语义是:"我声称这是重写,请编译器帮我验证"——但如果父类方法已经不存在了,编译器会报错。这里的问题是:父类还有一个同名的 getUser 方法(来自更上层的祖先类 Object 的 toString 等)——不,等等,让我们重新看:
// 实际情况:父类改名后,子类的 @Override 应该报错
// 但如果父类有一个 getUser 的默认实现(来自接口)...
// 接口(第三方库,没有改)
public interface UserRepository {
default User getUser(Long id) { // ★ 接口有 default 方法!
return null;
}
}
// 父类(重构改名)
public abstract class BaseService implements UserRepository {
@Override
public User findUser(Long id) { return defaultUser(); }
// getUser 继承自接口的 default 方法
}
// 子类
public class UserService extends BaseService {
@Override
public User getUser(Long id) { // ★ 重写的是接口 default 方法,不是父类方法
return userRepository.findById(id);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
真正的陷阱:@Override 编译通过了——因为子类确实重写了接口的 default 方法。但调用方用的是 BaseService.findUser(),子类的 getUser 永远不会被调用。
事故根因:@Override 只保证"这是一个重写",不保证"你重写的是你想重写的那个方法"。
# 1.2 Lombok 魔法失灵
同一周,另一个团队的 Lombok @Data 注解突然失效——编译报错 cannot find symbol: method getId():
@Data
public class OrderDTO {
private Long id;
private String orderNo;
private BigDecimal amount;
}
// 调用方
OrderDTO dto = new OrderDTO();
dto.getId(); // ★ 编译报错:cannot find symbol
2
3
4
5
6
7
8
9
10
排查过程:
步骤 1:检查 Lombok 依赖 → pom.xml 里有 ✅
步骤 2:检查 IDE 插件 → Lombok Plugin 已安装 ✅
步骤 3:检查 annotationProcessorPaths → ★ 缺失!
<!-- 错误的 pom.xml -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>compile</scope> <!-- ★ 只是普通依赖,不是注解处理器 -->
</dependency>
<!-- 正确的 pom.xml -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
根因:Lombok 不是普通的运行时库——它是一个编译期注解处理器,必须在 annotationProcessorPaths 中声明,才能在 javac 编译阶段被触发。
事故复盘抛出 7 个追问:
追问 ①:注解到底是什么?它和普通接口有什么区别?
追问 ②:@Override 为什么能在编译期生效?它的 Retention 是什么?
追问 ③:APT 注解处理器是怎么工作的?它在哪个阶段介入?
追问 ④:Lombok 为什么能"凭空生成"getter/setter?它修改了什么?
追问 ⑤:运行时 @Autowired 是怎么被 Spring 读取的?
追问 ⑥:Spring 的 @SpringBootApplication 是怎么组合多个注解的?
追问 ⑦:自定义注解怎么设计才不踩坑?
2
3
4
5
6
7
# 1.3 我们要回答什么
第 26 篇要把"注解从定义到处理的完整生命周期"讲透——注解有两条完全不同的处理路径:
注解的两条处理路径:
路径 A:编译期处理(SOURCE / CLASS Retention)
javac 编译 → 触发 APT 注解处理器 → 生成/修改代码 → 再次编译
典型代表:@Override(编译器内置)、Lombok(修改 AST)、MapStruct(生成实现类)
路径 B:运行时处理(RUNTIME Retention)
JVM 加载 class → 注解信息保留在字节码 → 反射读取 → 框架处理
典型代表:@Autowired(Spring 反射注入)、@Transactional(AOP 代理)、@RequestMapping(路由注册)
2
3
4
5
6
7
8
9
带着 7 个核心问题展开:
① 注解是什么类型? → 第2章
② @Retention 三种策略的本质区别? → 第3章
③ APT 注解处理器怎么工作? → 第4章
④ Lombok 为什么能生成代码? → 第5章
⑤ 运行时注解怎么被读取? → 第6章
⑥ Spring 组合注解怎么实现? → 第7章
⑦ 自定义注解怎么设计不踩坑? → 第8、9章
2
3
4
5
6
7
本篇路线:
注解本质 (第2章) ─── @interface 即特殊接口
↓
元注解 (第3章) ←—— @Retention/@Target/@Inherited/@Repeatable
↓
编译期路径 (第4章) ←—— APT 注解处理器
Lombok (第5章) ←—— 修改 AST 的黑魔法
↓
运行时路径 (第6章) ←—— 反射读取注解代理对象
Spring 注解 (第7章) ←—— 组合注解 + @AliasFor
↓
自定义注解 (第8章) ←—— 参数校验/权限/缓存三实战
实战陷阱 (第9章) ←—— 4 大坑
↓
综合案例串讲 (第10章)
2
3
4
5
6
7
8
9
10
11
12
13
14
# 2. 注解的本质
# 2.1 注解即接口
写一个最简单的注解:
public @interface MyAnnotation {
String value() default "";
int count() default 1;
}
2
3
4
用 javap -p MyAnnotation.class 反编译:
public interface MyAnnotation extends java.lang.annotation.Annotation {
public abstract java.lang.String value();
public abstract int count();
}
2
3
4
真相:@interface 就是一个特殊的接口——编译器把它翻译成 interface extends java.lang.annotation.Annotation。
java.lang.annotation.Annotation 是所有注解的父接口:
public interface Annotation {
boolean equals(Object obj);
int hashCode();
String toString();
Class<? extends Annotation> annotationType(); // ★ 返回注解的 Class
}
2
3
4
5
6
注解的"属性"实际上是接口的"抽象方法"——value() 就是一个返回 String 的抽象方法,default "" 是 Java 接口的默认值语法(注解专用,不是 JDK 8 的 default 方法)。
疑惑:既然是接口,谁来实现它?
论证——运行时注解由 JDK 动态代理实现(§6.2 详解):
// 你写的
@MyAnnotation(value = "hello", count = 3)
public class Foo { }
// JVM 在运行时创建的代理对象(伪代码)
class $Proxy0 implements MyAnnotation {
public String value() { return "hello"; }
public int count() { return 3; }
public Class<? extends Annotation> annotationType() { return MyAnnotation.class; }
}
2
3
4
5
6
7
8
9
10
结论:注解是接口,注解实例是动态代理对象——这是理解"运行时注解读取"的基础。
# 2.2 编译产物解析
注解信息如何存储在 class 文件中?
class 文件结构(第 13 篇已讲)中有两个专门的属性表:
RuntimeVisibleAnnotations ← RUNTIME Retention 的注解
RuntimeInvisibleAnnotations ← CLASS Retention 的注解(运行时不可见)
2
用 javap -verbose Foo.class 查看:
RuntimeVisibleAnnotations:
0: #12(#13=s#14)
com.example.MyAnnotation(
value="hello"
)
2
3
4
5
关键事实:
RUNTIME注解 → 写入RuntimeVisibleAnnotations→ 运行时可通过反射读取CLASS注解 → 写入RuntimeInvisibleAnnotations→ 字节码里有,但反射读不到SOURCE注解 → 不写入 class 文件 → 编译后彻底消失
这就是 §1.1 中 @Override 的秘密:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE) // ★ SOURCE!编译后消失
public @interface Override { }
2
3
@Override 是 SOURCE 级别——它只存在于源码阶段,供编译器检查,编译完就消失——运行时根本看不到它。
# 2.3 注解与注释的区别
一个常见的认知误区——注解(Annotation)和注释(Comment)的本质差异:
| 维度 | 注释 // /* */ | 注解 @Xxx |
|---|---|---|
| 存在阶段 | 仅源码 | 源码/字节码/运行时(取决于 Retention) |
| 处理者 | 无(人类阅读) | 编译器 / APT / JVM / 框架 |
| 类型系统 | 无类型 | 强类型(接口) |
| 可携带数据 | 纯文本 | 结构化属性(有类型约束) |
| 可被程序读取 | 否 | 是(反射) |
| 影响程序行为 | 否 | 是(通过处理器) |
结论:注解是"给程序读的元数据",注释是"给人读的说明"——两者在语言层面完全不同。
# 2.4 注解的三大用途
注解在 Java 生态中的三大核心用途:
用途 1:编译器指令(SOURCE Retention)
@Override → 检查重写正确性
@Deprecated → 标记废弃,触发编译警告
@SuppressWarnings → 抑制特定警告
@FunctionalInterface → 检查函数式接口约束
用途 2:编译期代码生成(SOURCE/CLASS Retention + APT)
Lombok @Data → 生成 getter/setter/equals/hashCode/toString
MapStruct @Mapper → 生成类型转换实现类
Dagger @Inject → 生成依赖注入代码
AutoValue @AutoValue → 生成不可变值类
用途 3:运行时元数据(RUNTIME Retention + 反射)
Spring @Autowired → 依赖注入
Spring @Transactional → 事务代理
JPA @Entity → ORM 映射
JUnit @Test → 测试框架识别
Jackson @JsonProperty → JSON 序列化映射
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
结论:注解本身不做任何事——它只是标记。真正做事的是"注解处理器"——编译期的 APT 或运行时的反射框架。
# 3. 元注解全解
# 3.1 @Retention 保留策略
@Retention 决定注解在哪个阶段存活——这是最重要的元注解:
public enum RetentionPolicy {
SOURCE, // 仅源码,编译后消失
CLASS, // 保留到 class 文件,但运行时不可见(默认值)
RUNTIME // 保留到运行时,可通过反射读取
}
2
3
4
5
三种策略的生命周期:
SOURCE: 源码 ──→ 编译 ──→ [消失]
CLASS: 源码 ──→ 编译 ──→ class文件 ──→ 类加载 ──→ [不可见]
RUNTIME: 源码 ──→ 编译 ──→ class文件 ──→ 类加载 ──→ 运行时可见
2
3
疑惑:CLASS 策略有什么用?既然运行时不可见,为什么不直接用 SOURCE?
论证——CLASS 策略的典型用途是字节码增强工具:
// 字节码增强工具(如 ASM、ByteBuddy)在类加载前读取 class 文件
// 它们不通过反射,而是直接解析字节码
// CLASS 注解对它们可见,对运行时反射不可见
// 典型例子:
@Retention(RetentionPolicy.CLASS)
public @interface NotNull { } // 字节码分析工具用,运行时不需要
// 对比:
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull { } // 运行时校验框架用(如 Hibernate Validator)
2
3
4
5
6
7
8
9
10
11
结论:
- SOURCE → 编译器指令(@Override、@SuppressWarnings)
- CLASS → 字节码工具(ASM 分析、FindBugs 静态分析)
- RUNTIME → 运行时框架(Spring、JPA、JUnit)
§1.1 的根因:@Override 是 SOURCE,编译后消失——运行时无法通过反射检查"某方法是否有 @Override"。
# 3.2 @Target 作用目标
@Target 限制注解可以标注在哪些元素上:
public enum ElementType {
TYPE, // 类、接口、枚举、注解
FIELD, // 字段(包括枚举常量)
METHOD, // 方法
PARAMETER, // 方法参数
CONSTRUCTOR, // 构造器
LOCAL_VARIABLE, // 局部变量
ANNOTATION_TYPE, // 注解类型(元注解用)
PACKAGE, // 包(package-info.java)
TYPE_PARAMETER, // 泛型类型参数(JDK 8+)
TYPE_USE, // 类型使用(JDK 8+,最广泛)
MODULE, // 模块(JDK 9+)
RECORD_COMPONENT // Record 组件(JDK 16+)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
JDK 8 新增的 TYPE_USE——可以标注在任何类型使用的地方:
@Target(ElementType.TYPE_USE)
public @interface NonNull { }
// 可以用在:
@NonNull String name; // 字段类型
List<@NonNull String> list; // 泛型参数
void method(@NonNull String s) { } // 参数类型
@NonNull String method() { } // 返回类型
(@NonNull String) obj // 类型转换
new @NonNull String("hello") // 对象创建
2
3
4
5
6
7
8
9
10
疑惑:不写 @Target 会怎样?
论证——不写 @Target 意味着可以标注在任何地方(除了 TYPE_PARAMETER 和 TYPE_USE 需要显式声明)。这通常是设计失误——过于宽泛的 @Target 会让注解被误用。
最佳实践:永远显式声明 @Target——精确限制使用范围,让 IDE 和编译器帮你防止误用。
# 3.3 @Inherited 继承语义
@Inherited 是一个容易被误解的元注解:
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyAnnotation { }
@MyAnnotation
public class Parent { }
public class Child extends Parent { }
// 运行时
Child.class.isAnnotationPresent(MyAnnotation.class); // ★ true!
Child.class.getDeclaredAnnotation(MyAnnotation.class); // ★ null(直接声明的没有)
Child.class.getAnnotation(MyAnnotation.class); // ★ 非null(继承来的)
2
3
4
5
6
7
8
9
10
11
12
13
14
关键限制——@Inherited 只对类继承有效,对接口实现无效:
@MyAnnotation
public interface MyInterface { }
public class Impl implements MyInterface { }
Impl.class.isAnnotationPresent(MyAnnotation.class); // ★ false!接口不传递
2
3
4
5
6
疑惑:为什么接口不传递?
论证——Java 允许多接口实现,如果多个接口都有同名注解但属性不同,继承语义会产生歧义——JDK 设计者选择了"不传递"来避免这个问题。
结论:@Inherited 只在类继承链上传递,接口实现不传递——Spring 的 @SpringBootApplication 等组合注解不依赖 @Inherited,而是用自己的注解扫描机制(§7.1)。
# 3.4 @Repeatable 重复注解
JDK 8 之前,同一个注解不能在同一个元素上标注两次。JDK 8 引入 @Repeatable:
// 容器注解(必须先定义)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Schedules {
Schedule[] value(); // ★ 容器必须有 value() 数组
}
// 可重复注解
@Repeatable(Schedules.class) // ★ 指定容器
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Schedule {
String cron();
}
// 使用
@Schedule(cron = "0 0 * * * ?")
@Schedule(cron = "0 30 * * * ?")
public void task() { }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
编译器的处理——编译器把多个 @Schedule 自动包装成一个 @Schedules:
// 编译后等价于:
@Schedules({
@Schedule(cron = "0 0 * * * ?"),
@Schedule(cron = "0 30 * * * ?")
})
public void task() { }
2
3
4
5
6
读取方式:
// 读取重复注解
Schedule[] schedules = method.getAnnotationsByType(Schedule.class); // ★ JDK 8 新 API
// 或
Schedules container = method.getAnnotation(Schedules.class);
Schedule[] schedules2 = container.value();
2
3
4
5
结论:@Repeatable 是语法糖——底层仍然是容器注解包数组,读取时用 getAnnotationsByType 而不是 getAnnotation。
# 4. 编译期处理 APT
# 4.1 APT 工作原理
APT(Annotation Processing Tool)是 Java 编译流程中的一个扩展点——在 javac 编译的特定阶段,允许自定义代码介入:
javac 编译流程(含 APT):
1. 解析源码 → 生成 AST(抽象语法树)
2. ★ 触发注解处理器(APT 阶段)
├── 扫描所有注解
├── 找到对应的注解处理器
├── 处理器可以:
│ ├── 生成新的源文件(.java)
│ ├── 生成新的 class 文件
│ └── 生成资源文件
└── 如果生成了新文件 → 重新进入步骤 1(多轮处理)
3. 语义分析(类型检查、符号解析)
4. 字节码生成 → .class 文件
2
3
4
5
6
7
8
9
10
11
12
13
关键事实:APT 处理器只能生成新文件,不能修改已有源文件(这是官方 API 的限制)——Lombok 绕过了这个限制(§5.2)。
APT 的触发方式:
方式 1:javac 命令行
javac -processor com.example.MyProcessor Foo.java
方式 2:META-INF/services(SPI 机制,第 51 篇)
META-INF/services/javax.annotation.processing.Processor
内容:com.example.MyProcessor
方式 3:Maven annotationProcessorPaths(§1.2 的修复方案)
2
3
4
5
6
7
8
# 4.2 注解处理器接口
实现一个注解处理器需要继承 AbstractProcessor:
public abstract class AbstractProcessor implements Processor {
// ★ 声明处理哪些注解(支持通配符 "*")
public abstract Set<String> getSupportedAnnotationTypes();
// ★ 声明支持的 Java 版本
public abstract SourceVersion getSupportedSourceVersion();
// ★ 核心处理方法
public abstract boolean process(
Set<? extends TypeElement> annotations, // 本轮要处理的注解
RoundEnvironment roundEnv // 本轮的环境(包含所有被注解的元素)
);
// 工具对象(由编译器注入)
protected ProcessingEnvironment processingEnv;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
this.processingEnv = processingEnv;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ProcessingEnvironment 提供的工具:
processingEnv.getElementUtils() // 操作 Java 元素(类、方法、字段)
processingEnv.getTypeUtils() // 操作类型系统
processingEnv.getFiler() // 创建新文件(源码/class/资源)
processingEnv.getMessager() // 输出编译消息(警告/错误)
2
3
4
# 4.3 手写一个处理器
实现一个简单的 @Builder 注解处理器——自动生成 Builder 类:
@SupportedAnnotationTypes("com.example.Builder")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@AutoService(Processor.class) // Google AutoService 自动生成 SPI 配置
public class BuilderProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(annotation);
for (Element element : elements) {
if (element.getKind() != ElementKind.CLASS) continue;
TypeElement classElement = (TypeElement) element;
generateBuilder(classElement);
}
}
return true; // ★ true 表示"我处理了这个注解,其他处理器不用再处理"
}
private void generateBuilder(TypeElement classElement) {
String className = classElement.getSimpleName().toString();
String packageName = processingEnv.getElementUtils()
.getPackageOf(classElement).getQualifiedName().toString();
// 收集所有字段
List<VariableElement> fields = classElement.getEnclosedElements().stream()
.filter(e -> e.getKind() == ElementKind.FIELD)
.map(e -> (VariableElement) e)
.collect(Collectors.toList());
// 生成 Builder 源码
StringBuilder sb = new StringBuilder();
sb.append("package ").append(packageName).append(";\n\n");
sb.append("public class ").append(className).append("Builder {\n");
for (VariableElement field : fields) {
String fieldName = field.getSimpleName().toString();
String fieldType = field.asType().toString();
sb.append(" private ").append(fieldType).append(" ").append(fieldName).append(";\n");
}
sb.append("\n");
for (VariableElement field : fields) {
String fieldName = field.getSimpleName().toString();
String fieldType = field.asType().toString();
sb.append(" public ").append(className).append("Builder ")
.append(fieldName).append("(").append(fieldType).append(" ").append(fieldName).append(") {\n")
.append(" this.").append(fieldName).append(" = ").append(fieldName).append(";\n")
.append(" return this;\n")
.append(" }\n\n");
}
sb.append(" public ").append(className).append(" build() {\n")
.append(" ").append(className).append(" obj = new ").append(className).append("();\n");
for (VariableElement field : fields) {
String fieldName = field.getSimpleName().toString();
sb.append(" obj.set").append(capitalize(fieldName))
.append("(").append(fieldName).append(");\n");
}
sb.append(" return obj;\n }\n}\n");
// ★ 写入新文件
try {
JavaFileObject file = processingEnv.getFiler()
.createSourceFile(packageName + "." + className + "Builder");
try (Writer w = file.openWriter()) {
w.write(sb.toString());
}
} catch (IOException e) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
使用效果:
@Builder
public class User {
private Long id;
private String name;
}
// 编译后自动生成 UserBuilder.java:
User user = new UserBuilder()
.id(1L)
.name("Alice")
.build();
2
3
4
5
6
7
8
9
10
11
# 4.4 多轮处理机制
APT 支持多轮处理——处理器生成的新文件可能也包含注解,需要再次处理:
第 1 轮:
输入:User.java(有 @Builder)
处理:BuilderProcessor 生成 UserBuilder.java
第 2 轮:
输入:UserBuilder.java(如果也有注解)
处理:继续处理
最后一轮(finalRound):
roundEnv.processingOver() == true
处理器做收尾工作(如生成汇总文件)
2
3
4
5
6
7
8
9
10
11
检查最后一轮:
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
// ★ 最后一轮,生成汇总文件
generateSummaryFile();
return false;
}
// 正常处理
}
2
3
4
5
6
7
8
9
结论:APT 是 Java 编译流程的官方扩展点——MapStruct、Dagger、AutoValue 等框架都基于此,生成零反射的高性能代码。
# 5. Lombok 字节码魔法
# 5.1 Lombok 工作时机
Lombok 也是注解处理器——但它做了一件官方 API 不允许的事:修改已有源文件的 AST。
官方 APT 的限制:处理器只能通过 Filer.createSourceFile() 创建新文件,不能修改已有文件。
Lombok 的突破:通过 com.sun.tools.javac.tree.JCTree(JDK 内部 API,非公开)直接操作 AST:
Lombok 工作流程:
1. javac 解析 User.java → 生成 AST
2. ★ Lombok 注解处理器被触发
3. Lombok 通过 Trees.instance(processingEnv) 获取 AST 引用
4. ★ 直接修改 AST(添加 getter/setter/构造器等节点)
5. javac 继续编译(看到的 AST 已经被 Lombok 修改过了)
6. 生成包含 getter/setter 的 class 文件
2
3
4
5
6
7
8
关键 API(Lombok 内部使用,非公开):
// Lombok 内部代码(简化)
JavacTrees trees = JavacTrees.instance(processingEnv);
JCTree.JCClassDecl classDecl = (JCTree.JCClassDecl) trees.getTree(classElement);
// 生成 getter 方法的 AST 节点
JCTree.JCMethodDecl getter = makeGetter(fieldDecl);
// ★ 直接把 getter 节点插入类的 AST
classDecl.defs = classDecl.defs.append(getter);
2
3
4
5
6
7
8
9
# 5.2 修改 AST 的黑魔法
为什么说是"黑魔法":
- 使用了 JDK 内部 API(
com.sun.tools.javac.*)——这些 API 在 JDK 9 模块化后被封装,需要--add-opens才能访问 - 违反了 APT 的设计契约——APT 设计为"只读已有代码,只写新文件"
- 不同 JDK 版本的内部 API 可能变化——Lombok 每次 JDK 升级都要跟进适配
Lombok 的 JDK 版本适配问题:
JDK 8 → Lombok 1.16.x
JDK 11 → Lombok 1.18.x(需要 --add-opens)
JDK 17 → Lombok 1.18.20+(更多 --add-opens)
JDK 21 → Lombok 1.18.30+(虚拟线程相关适配)
2
3
4
Maven 配置(JDK 17+):
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
<arg>--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
<arg>--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
<arg>--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
<arg>--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
</compilerArgs>
</configuration>
</plugin>
2
3
4
5
6
7
8
9
10
11
12
13
14
# 5.3 @Data 展开全貌
@Data 是 Lombok 最常用的注解——它等价于:
@Data
public class User {
private Long id;
private String name;
private int age;
}
// 等价于(Lombok 生成的代码):
public class User {
private Long id;
private String name;
private int age;
// @Getter:所有字段
public Long getId() { return id; }
public String getName() { return name; }
public int getAge() { return age; }
// @Setter:所有非 final 字段
public void setId(Long id) { this.id = id; }
public void setName(String name) { this.name = name; }
public void setAge(int age) { this.age = age; }
// @RequiredArgsConstructor:final 字段的构造器(这里没有 final 字段,生成无参构造)
public User() { }
// @EqualsAndHashCode:基于所有非 static 非 transient 字段
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User other = (User) o;
return Objects.equals(id, other.id)
&& Objects.equals(name, other.name)
&& age == other.age;
}
@Override
public int hashCode() {
return Objects.hash(id, name, age);
}
// @ToString:所有字段
@Override
public String toString() {
return "User(id=" + id + ", name=" + name + ", age=" + age + ")";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Data 的常见陷阱:
// 陷阱 1:@Data 在继承场景下 equals/hashCode 不包含父类字段
@Data
@EqualsAndHashCode(callSuper = true) // ★ 必须显式声明
public class VipUser extends User {
private String vipLevel;
}
// 陷阱 2:@Data 生成的 equals 用 instanceof,不是 getClass()
// 子类实例 equals 父类实例可能返回 true(取决于字段)
// 陷阱 3:@Data 不适合 JPA Entity
// JPA 要求 equals/hashCode 基于 id,但 @Data 基于所有字段
// 推荐 JPA Entity 手写 equals/hashCode 或用 @EqualsAndHashCode(onlyExplicitlyIncluded = true)
2
3
4
5
6
7
8
9
10
11
12
13
# 5.4 Lombok 的争议
Lombok 在工程界有持续的争议:
支持方:
- 消除大量样板代码(getter/setter/toString/equals/hashCode)
- 提升代码可读性(关注业务字段,不被样板淹没)
@Builder生成流式 API,比手写更规范
反对方:
- 使用 JDK 内部 API,升级风险高
- IDE 支持依赖插件,调试困难(生成的代码不可见)
- 隐藏了代码细节,新人难以理解
- JDK 16+ 的
record已经原生支持不可变数据类
现代 Java 的替代方案:
// JDK 16+ record 替代 @Data(不可变场景)
public record User(Long id, String name, int age) { }
// 自动生成:构造器、getter(id()/name()/age())、equals/hashCode/toString
// JDK 14+ @lombok.Builder 替代方案:手写 Builder(IDE 可生成)
2
3
4
5
结论:Lombok 是工程权衡——新项目建议优先用 record + 手写,老项目 Lombok 已经用了就继续用,但要锁定版本。
# 6. 运行时反射读取
# 6.1 AnnotatedElement 接口
运行时读取注解的入口是 java.lang.reflect.AnnotatedElement 接口——Class、Method、Field、Constructor 都实现了它:
public interface AnnotatedElement {
// 获取直接声明的注解(不包括继承来的)
<T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass);
Annotation[] getDeclaredAnnotations();
// 获取注解(包括 @Inherited 继承来的)
<T extends Annotation> T getAnnotation(Class<T> annotationClass);
Annotation[] getAnnotations();
// JDK 8+:获取重复注解
<T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass);
<T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass);
// 快速检查
default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
return getAnnotation(annotationClass) != null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
使用示例:
@MyAnnotation(value = "hello", count = 3)
public class Foo {
@Deprecated
public void oldMethod() { }
}
// 读取类注解
MyAnnotation ann = Foo.class.getAnnotation(MyAnnotation.class);
System.out.println(ann.value()); // "hello"
System.out.println(ann.count()); // 3
// 读取方法注解
Method m = Foo.class.getMethod("oldMethod");
boolean deprecated = m.isAnnotationPresent(Deprecated.class); // true
2
3
4
5
6
7
8
9
10
11
12
13
14
# 6.2 注解代理对象
§2.1 提到注解实例是动态代理——现在深入看实现:
// JDK 内部:AnnotationParser.annotationForMap
static Annotation annotationForMap(Class<? extends Annotation> type,
Map<String, Object> memberValues) {
return (Annotation) Proxy.newProxyInstance(
type.getClassLoader(),
new Class[]{ type },
new AnnotationInvocationHandler(type, memberValues) // ★ 代理处理器
);
}
2
3
4
5
6
7
8
9
AnnotationInvocationHandler 的核心逻辑:
class AnnotationInvocationHandler implements InvocationHandler {
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues; // ★ 存储注解属性值
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
// 处理 Annotation 接口的方法
if (member.equals("annotationType")) return type;
if (member.equals("equals")) return equalsImpl(proxy, args[0]);
if (member.equals("hashCode")) return hashCodeImpl();
if (member.equals("toString")) return toStringImpl();
// ★ 处理注解属性方法(如 value()、count())
Object result = memberValues.get(member);
if (result == null) {
// 使用默认值
result = method.getDefaultValue();
}
return result;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
关键事实:注解属性值存储在 Map 中,每次调用 ann.value() 都是一次 Map 查找——这就是为什么注解读取有性能开销,热点路径需要缓存(§6.4)。
# 6.3 继承与组合注解
注解继承的限制——注解不能 extends 另一个注解:
// ❌ 编译报错:注解不能继承注解
public @interface MyAnnotation extends OtherAnnotation { }
2
Spring 的解决方案——组合注解(§7.1 详解):
// 把多个注解组合成一个
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Controller
@ResponseBody
public @interface RestController {
@AliasFor(annotation = Controller.class)
String value() default "";
}
2
3
4
5
6
7
8
9
读取组合注解——标准反射 API 只能读到直接声明的注解,Spring 提供了 AnnotationUtils:
// 标准反射:只读直接注解
RestController ann = Foo.class.getAnnotation(RestController.class); // 有
Controller ctrl = Foo.class.getAnnotation(Controller.class); // ★ null!
// Spring AnnotationUtils:递归读取组合注解
Controller ctrl2 = AnnotationUtils.findAnnotation(Foo.class, Controller.class); // 有
2
3
4
5
6
# 6.4 性能优化策略
注解反射读取的性能开销来自两个地方:
- 反射本身的开销(第 07 篇已讲)——
getAnnotation内部有同步和类型检查 - 代理对象的 Map 查找——每次属性访问都是 HashMap.get
优化策略:
// ❌ 热点路径每次读取
public void handle(Method method) {
MyAnnotation ann = method.getAnnotation(MyAnnotation.class); // 每次反射
String value = ann.value();
}
// ✅ 启动时缓存
private static final Map<Method, MyAnnotation> CACHE = new ConcurrentHashMap<>();
public void handle(Method method) {
MyAnnotation ann = CACHE.computeIfAbsent(method,
m -> m.getAnnotation(MyAnnotation.class));
String value = ann.value();
}
// ✅ 更进一步:缓存属性值而不是注解对象
private static final Map<Method, String> VALUE_CACHE = new ConcurrentHashMap<>();
public void handle(Method method) {
String value = VALUE_CACHE.computeIfAbsent(method, m -> {
MyAnnotation ann = m.getAnnotation(MyAnnotation.class);
return ann != null ? ann.value() : "";
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Spring 的做法——AnnotationUtils 内部有 AnnotationCache,基于 ConcurrentReferenceHashMap(软引用,内存压力时可回收)缓存所有注解查找结果。
结论:框架层面的注解读取必须缓存——Spring 启动时扫描所有注解并缓存,运行时直接查缓存,不再反射。
# 7. Spring 注解体系
# 7.1 组合注解原理
Spring 的注解体系建立在组合注解(Meta-Annotation)的概念上——一个注解可以被另一个注解标注:
// @SpringBootApplication 的定义
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration // ← 元注解(标注在注解上的注解)
@EnableAutoConfiguration // ← 元注解
@ComponentScan(excludeFilters = { ... }) // ← 元注解
public @interface SpringBootApplication {
// ...
}
// @SpringBootConfiguration 的定义
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration // ← 又一层元注解
@Indexed
public @interface SpringBootConfiguration { }
// @Configuration 的定义
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // ← 最终的核心注解
public @interface Configuration { }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
注解继承链:
@SpringBootApplication
└── @SpringBootConfiguration
└── @Configuration
└── @Component ← 最终被 Spring 识别
2
3
4
Spring 的注解扫描——AnnotationUtils.findAnnotation 递归查找:
// Spring 内部(简化)
public static <A extends Annotation> A findAnnotation(Class<?> clazz, Class<A> annotationType) {
// 1. 直接查找
A ann = clazz.getDeclaredAnnotation(annotationType);
if (ann != null) return ann;
// 2. 递归查找元注解
for (Annotation metaAnn : clazz.getDeclaredAnnotations()) {
if (isInJavaLangAnnotationPackage(metaAnn)) continue; // 跳过 JDK 内置元注解
ann = findAnnotation(metaAnn.annotationType(), annotationType); // ★ 递归
if (ann != null) return ann;
}
// 3. 查找父类(@Inherited 语义)
Class<?> superclass = clazz.getSuperclass();
if (superclass != null && superclass != Object.class) {
return findAnnotation(superclass, annotationType);
}
return null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 7.2 @AliasFor 别名机制
Spring 的 @AliasFor 解决了组合注解中属性传递的问题:
// 问题:@RequestMapping 有 value 和 path 属性
@RequestMapping(value = "/api/users")
// 等价于
@RequestMapping(path = "/api/users")
// @AliasFor 声明两个属性互为别名
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
@AliasFor("path")
String[] value() default {};
@AliasFor("value")
String[] path() default {};
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
跨注解的 @AliasFor——组合注解中覆盖元注解的属性:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@RequestMapping // 元注解
public @interface RestController {
// ★ 把 RestController.value 映射到 RequestMapping.path
@AliasFor(annotation = RequestMapping.class, attribute = "path")
String[] value() default {};
}
// 使用
@RestController("/api") // 等价于 @RequestMapping(path = "/api")
public class UserController { }
2
3
4
5
6
7
8
9
10
11
12
13
@AliasFor 的实现——Spring 在读取注解时,通过 AnnotationUtils.synthesizeAnnotation 创建一个合成注解代理,代理中处理别名逻辑:
// Spring 内部(简化)
// 读取 @RestController("/api") 上的 @RequestMapping
RequestMapping rm = AnnotationUtils.findAnnotation(UserController.class, RequestMapping.class);
rm.path(); // ★ 返回 ["/api"](从 @RestController.value 传递过来)
2
3
4
# 7.3 注解扫描与缓存
Spring 启动时的注解扫描流程:
Spring 启动流程(注解相关):
1. ClassPathScanningCandidateComponentProvider
└── 扫描 classpath 下所有 .class 文件
└── 用 ASM 读取字节码(不加载类!)检查是否有 @Component 等注解
└── 找到候选类 → 加入 BeanDefinition 列表
2. ConfigurationClassPostProcessor
└── 处理 @Configuration 类
└── 解析 @Bean、@Import、@ComponentScan 等
└── 递归处理组合注解
3. AutowiredAnnotationBeanPostProcessor
└── 扫描所有 Bean 的字段/方法
└── 找到 @Autowired/@Value → 注入依赖
4. ★ 所有注解信息缓存到 AnnotationMetadata
└── 后续请求直接查缓存,不再反射
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
关键优化——Spring 用 ASM 读字节码而不是反射加载类:
// Spring 内部:SimpleMetadataReader 用 ASM 读 class 文件
// 好处:不触发类初始化,不执行 static 块,速度快
// 坏处:只能读注解信息,不能执行代码
2
3
# 7.4 注解驱动的 AOP
@Transactional 是注解驱动 AOP 的典型案例:
@Transactional 的处理流程:
1. 启动时:
TransactionInterceptor 注册为 BeanPostProcessor
2. Bean 创建时:
检查 Bean 的类/方法是否有 @Transactional
如果有 → 创建 CGLIB/JDK 代理包裹原 Bean
3. 运行时:
调用 userService.save()
→ 实际调用 CGLIB 代理的 save()
→ 代理检查 @Transactional 属性(propagation/isolation/rollbackFor)
→ 开启事务
→ 调用原始 save()
→ 提交/回滚事务
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
注解属性的运行时读取:
// Spring TransactionAspectSupport 内部(简化)
TransactionAttribute txAttr =
annotationParser.parseTransactionAnnotation(method); // ★ 读取 @Transactional 属性
// AnnotationTransactionAttributeSource 内部
Transactional ann = AnnotationUtils.findAnnotation(method, Transactional.class);
if (ann != null) {
return new RuleBasedTransactionAttribute(
ann.propagation().value(),
ann.isolation().value(),
ann.timeout(),
ann.readOnly(),
// ...
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
结论:Spring 的注解驱动 AOP 本质是"运行时读取注解属性 → 动态代理拦截 → 按属性执行增强逻辑"——注解是配置,代理是执行。
# 8. 自定义注解实战
# 8.1 参数校验注解
实现一个 @PhoneNumber 校验注解:
// 1. 定义注解
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class) // JSR-303 约束注解
public @interface PhoneNumber {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String pattern() default "^1[3-9]\\d{9}$";
}
// 2. 实现校验器
public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {
private Pattern pattern;
@Override
public void initialize(PhoneNumber annotation) {
this.pattern = Pattern.compile(annotation.pattern());
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) return true; // null 由 @NotNull 处理
return pattern.matcher(value).matches();
}
}
// 3. 使用
public class UserDTO {
@PhoneNumber
private String phone;
@PhoneNumber(pattern = "^\\+86\\d{11}$", message = "需要带国际区号")
private String internationalPhone;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 8.2 权限控制注解
实现一个 @RequirePermission 注解 + AOP 拦截:
// 1. 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
String[] value(); // 需要的权限列表
boolean requireAll() default false; // true=需要所有权限,false=有一个即可
}
// 2. AOP 切面
@Aspect
@Component
public class PermissionAspect {
@Autowired
private SecurityContext securityContext;
@Around("@annotation(requirePermission)")
public Object checkPermission(ProceedingJoinPoint pjp,
RequirePermission requirePermission) throws Throwable {
String[] required = requirePermission.value();
Set<String> userPerms = securityContext.getCurrentUserPermissions();
boolean hasPermission;
if (requirePermission.requireAll()) {
hasPermission = userPerms.containsAll(Arrays.asList(required));
} else {
hasPermission = Arrays.stream(required).anyMatch(userPerms::contains);
}
if (!hasPermission) {
throw new AccessDeniedException("权限不足,需要: " + Arrays.toString(required));
}
return pjp.proceed();
}
}
// 3. 使用
@RestController
public class OrderController {
@RequirePermission("order:delete")
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) { ... }
@RequirePermission(value = {"order:read", "order:export"}, requireAll = true)
@GetMapping("/export")
public byte[] export() { ... }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 8.3 缓存注解设计
实现一个简单的 @Cacheable 注解:
// 1. 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cacheable {
String key(); // SpEL 表达式,如 "#id" 或 "'user:' + #id"
int ttlSeconds() default 300; // 缓存过期时间
Class<?> valueType(); // 缓存值类型(用于反序列化)
}
// 2. AOP 切面
@Aspect
@Component
public class CacheAspect {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private ObjectMapper objectMapper;
@Around("@annotation(cacheable)")
public Object cache(ProceedingJoinPoint pjp, Cacheable cacheable) throws Throwable {
// 解析 SpEL key
String cacheKey = resolveKey(cacheable.key(), pjp);
// 查缓存
String cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return objectMapper.readValue(cached, cacheable.valueType());
}
// 执行原方法
Object result = pjp.proceed();
// 写缓存
if (result != null) {
redisTemplate.opsForValue().set(
cacheKey,
objectMapper.writeValueAsString(result),
cacheable.ttlSeconds(), TimeUnit.SECONDS
);
}
return result;
}
private String resolveKey(String keyExpr, ProceedingJoinPoint pjp) {
// SpEL 解析(简化)
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
// 绑定方法参数
MethodSignature sig = (MethodSignature) pjp.getSignature();
String[] paramNames = sig.getParameterNames();
Object[] args = pjp.getArgs();
for (int i = 0; i < paramNames.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
return parser.parseExpression(keyExpr).getValue(context, String.class);
}
}
// 3. 使用
@Service
public class UserService {
@Cacheable(key = "'user:' + #id", ttlSeconds = 600, valueType = User.class)
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# 8.4 注解设计原则
设计自定义注解的 5 条原则:
原则 1:明确 Retention
- 编译期检查 → SOURCE
- 字节码工具 → CLASS
- 运行时框架 → RUNTIME
永远不要用默认值(CLASS),因为大多数场景要么 SOURCE 要么 RUNTIME
原则 2:精确 Target
不要写 @Target({TYPE, METHOD, FIELD, PARAMETER, ...}) 一把梭
精确限制使用范围,让编译器帮你防止误用
原则 3:属性要有 default
除非属性是必填的,否则提供合理的默认值
减少使用方的样板代码
原则 4:注解名要是名词/形容词
@Cacheable(可缓存的)✅
@DoCache(动词)❌
@Cache(太宽泛)⚠️
原则 5:文档化处理器
注解本身不做任何事,必须在 Javadoc 中说明"谁来处理这个注解"
否则使用方不知道注解是否生效
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 9. 实战陷阱清单
# 9.1 Retention 选错
最常见的注解事故 —— 忘写 @Retention:
// ❌ 错误:缺 @Retention
public @interface MyAnno { }
@MyAnno
public class Service { }
Service.class.getAnnotation(MyAnno.class); // → null ★ 反射读不到!
2
3
4
5
6
7
§4.3 已分析——默认是 CLASS,不是 RUNTIME。
修复:
@Retention(RetentionPolicy.RUNTIME) // ★ 必须显式声明
public @interface MyAnno { }
2
红线:写自定义注解的第一步必须确认 @Retention——通常 RUNTIME 是想要的。
# 9.2 注解不能继承
§3.4 已经分析——@Inherited 不通过接口传递。
Spring 用户的常见困惑:
// 自定义元注解
@Inherited
@Retention(RUNTIME)
public @interface Auditable { }
// 标在接口上
@Auditable
public interface PaymentService { }
public class PaymentServiceImpl implements PaymentService { }
// 反射读取
PaymentServiceImpl.class.getAnnotation(Auditable.class); // → null
AnnotationUtils.findAnnotation(PaymentServiceImpl.class, Auditable.class); // → @Auditable ✅
2
3
4
5
6
7
8
9
10
11
12
13
14
生产建议:所有"穿透继承层"的注解查找都用 Spring AnnotationUtils.findAnnotation ——别用 JDK 原生 API。
# 9.3 反射读取性能坑
每次都调 getAnnotation 是性能陷阱(§7.3)。
生产代码必须缓存:
private static final ClassValue<List<Auditable>> CACHE = new ClassValue<>() {
@Override
protected List<Auditable> computeValue(Class<?> type) {
return Arrays.stream(type.getAnnotationsByType(Auditable.class))
.toList();
}
};
// 调用
List<Auditable> annos = CACHE.get(SomeService.class); // 第一次反射,之后命中缓存
2
3
4
5
6
7
8
9
10
ClassValue 是 JDK 的 Class 维度缓存工具——比 ConcurrentHashMap 更适合"以 Class 为键"的场景,且支持 Class 卸载时自动清理。
# 9.4 注解处理器调试
APT 调试是新手痛点——处理器在 javac 进程中运行,IDE 默认调试不到。
正确姿势——开 -J-Xrunjdwp 让 javac 监听调试端口:
javac -J-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 \
-processor com.example.MyProcessor \
Foo.java
2
3
然后 IDEA 远程附加到 5005 端口——可以打断点调试 Processor.process。
或者写测试——使用 Google Compile Testing:
@Test
void shouldGenerateBuilder() {
Compilation compilation = Compiler.javac()
.withProcessors(new BuilderProcessor())
.compile(JavaFileObjects.forResource("TestEntity.java"));
assertThat(compilation).succeeded();
assertThat(compilation)
.generatedSourceFile("com.example.TestEntityBuilder")
.hasSourceEquivalentTo(...);
}
2
3
4
5
6
7
8
9
10
11
# 10. 综合案例串讲
# 10.1 双案例真相揭晓
回到第 1 章双事故,逐条揭晓:
① @Override 谁来检查、编译完还在吗:@Override 是 SOURCE 注解,由 javac 在编译期的语义分析阶段检查方法签名是否匹配父类。检查完成后从 .class 中剥离,运行时不存在(§3.1、§4.2)。它的全部价值在编译期被消费完毕——是"给编译器的指令"。
② 注解在字节码里是什么:注解类型本身是带 ACC_ANNOTATION 标志位的特殊接口——继承 java.lang.annotation.Annotation。注解的"使用实例"存储在被标记元素的 RuntimeVisibleAnnotations(RUNTIME)或 RuntimeInvisibleAnnotations(CLASS)属性中——属性值存在常量池里(§2.1、§2.3)。运行时的注解对象是 JDK 通过 Proxy 动态生成的代理类(§2.2、§7.2)。
③ 为什么 §1.1 错误没被拦住:因为 Map<String, Object> 也是 Object 的子类——javac 检查 @Override 时认为"父类有签名兼容的方法"——但实际是重载而非重写。这是 @Override 的检查规则边界。修复方式是用编译器警告 -Xlint:overrides 并把 OrderHandler 的方法改为接收 Object 后内部强转——或者干脆把父类的方法签名也改成 Map。
④ APT 能修改源码吗:标准 APT 不能修改已有类,只能创建新文件(§5.1、§5.4)。Filer.createSourceFile 创建新 .java、Filer.createClassFile 创建新 .class——这是 javac 给 Processor 的全部能力。
⑤ Lombok 凭空生成方法的"黑魔法":Lombok 表面是 APT,底层强转 ProcessingEnvironment 为 javac 内部类拿到 JavacAST,直接修改 AST 树——加 getter/setter/equals 等节点(§6.1、§6.2)。这是越界但合法的操作——代价是依赖 javac 私有 API,JDK 升级容易爆炸。JDK 16 起的 Record 是更优雅的替代(§6.4)。
⑥ @Override 与 @Data 处理时机的不同:两者都是 SOURCE Retention——但处理时机完全不同。@Override 在"语义检查阶段"被消费——javac 内置规则;@Data 在"APT 阶段"被处理——APT 早于语义检查,所以 @Data 生成的方法能被后续阶段当作正常代码处理(§5.1、§6.2)。正是这个时机差让 Lombok 能"无中生有"。
⑦ Spring @Autowired 走哪条路径:RUNTIME 反射路径——Spring 启动时先用 ASM 扫描类路径(不加载类)找到所有候选 Bean,再 Class.forName 加载,然后反射读取 @Autowired/@Value 等 RUNTIME 注解(§4.4、§8.1)。与 §1.1 的 @Override(编译期)和 §1.2 的 @Data(APT 改 AST)走的是完全不同的路径——这就是注解三大处理路径的全景。
§1.2 死循环的修复:用 @EqualsAndHashCode(of = {"id"}) 限定参与字段——只用 id 比较,避开循环引用:
@Data
@EqualsAndHashCode(of = "id") // ★ 只用 id 算 equals
public class User {
private Long id;
private String name;
private Department department;
}
2
3
4
5
6
7
# 10.2 一个注解的一生
把 @Auditable("payment") 这个最普通的注解串成生命树,回扣本篇所有章节:
T 0 源码阶段
────────
@Auditable("payment")
public class PaymentService { ... }
★ 此时 @Auditable 是源码注释 + 类型引用
★ 写一个 .java 文件就完成第一步
T+1ms javac 编译开始
──────────────
[Parse] 源码 → AST
★ Auditable 被解析为 JCAnnotation 节点
T+5ms [Enter] 注册类型符号
T+10ms ★ APT 阶段开始(§5.1)
调用所有 Annotation Processor
- 如果是 SOURCE 注解(如 @Data)→ Lombok 改 AST(§6.2)
- 如果是 SOURCE 标记(如 @AutoBuilder)→ 生成新文件(§5.4)
- 如果是 CLASS/RUNTIME 注解 → 通常无 APT 处理
T+30ms [Analyse] 类型检查
★ 这里检查 @Override(§4.2)
★ @Override 处理完毕后从 AST 中移除(SOURCE)
T+50ms [Generate] 字节码生成
★ 根据 Retention 决定写入哪个属性:
SOURCE → 不写
CLASS → 写入 RuntimeInvisibleAnnotations(§4.3)
RUNTIME → 写入 RuntimeVisibleAnnotations(§4.4)
★ 注解属性值("payment")写入常量池
T+100ms .class 文件落盘
─────────────
PaymentService.class:
RuntimeVisibleAnnotations:
0: #20(#21=s#22)
Auditable(value="payment")
T+1day JVM 启动加载
──────────────
ClassLoader 加载 PaymentService([02]篇)
解析 RuntimeVisibleAnnotations 属性
★ 在 Class 内部 annotationData 中缓存(§7.3)
T+1day+10ms Spring 反射扫描(§8.1)
───────────────────────
ASM 读字节码 → 发现 @Component 系列 → 加入候选
Class.forName("PaymentService") 触发加载
反射读 @Autowired 字段 → 注入依赖
T+1day+50ms AOP 代理生成([07]篇)
────────────────────
Spring 发现 @Auditable → 创建动态代理
代理拦截方法调用 → 写审计日志
T+1day+100ms 用户请求到达
────────────────
method.getAnnotation(Auditable.class)
★ 第一次:JDK 用 Proxy 生成 $Proxy0 实现 Auditable 接口(§7.2)
★ 之后:从 ClassValue/ConcurrentHashMap 缓存命中(§7.3)
T+forever 注解的生命与 Class 共存亡
★ 类卸载时 annotationData 一同消亡
跨篇引用全景:
[02] ClassLoader 加载注解到方法区
[07] Annotation 通过动态代理实例化
[12] 异常注解 @SuppressWarnings 与 javac 协作
[24] hashCode/equals 在动态代理对象上的语义
[25] 注解就是带 ACC_ANNOTATION 的特殊枚举式接口
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# 10.3 设计哲学回扣
跳出技术细节,提炼贯穿注解设计的三条工程哲学:
元数据应是语言一等公民:注解出现前,元数据散落在 javadoc 注释、XML 配置、命名约定(如 EJB 2.x 的"_Bean"后缀)中——没有类型安全、没有 IDE 支持、没有统一处理 API。注解把元数据提升为"语言关键字 + 类型系统的一部分"——让元数据具备和代码同等的工具友好性。这是 Java 5 的革命性设计——之后 Spring 从 XML 时代走向注解时代、Servlet 从 web.xml 走向 @WebServlet——整个 Java 生态因此更新换代。这条哲学在第 30 篇的 Record 和 Sealed 中再次闪光——把"不可变数据载体"和"封闭继承层级"也升级为语言一等公民。
三态 Retention 是关键解耦:SOURCE/CLASS/RUNTIME 看似只是三个枚举值——实际是注解世界的彻底解耦。SOURCE 只服务编译器(@Override/@SuppressWarnings)、CLASS 只服务静态分析工具(@NonNull)、RUNTIME 才进入运行时(@Autowired)——每个 Retention 对应一类消费者,互不打扰。这是 Java 设计中"分层语义"的典范——同一个机制(注解)通过一个开关(Retention)服务三类完全不同的工具链。对照来看——线程模型也有"用户态/内核态"、GC 也有"年轻代/老年代/元空间"——分层让复杂系统可治理。
机制开放,策略上交框架:JDK 只提供"注解定义、注解读取、APT 钩子"三个基础机制——所有具体策略(什么注解干什么)全部交给框架和应用。Spring 用 @Component、JPA 用 @Entity、JUnit 用 @Test——JDK 不内置任何业务语义。这是 Unix 哲学"机制与策略分离"在 Java 中的体现——也是为什么注解从 JDK 5 至今没有大改——基础机制稳如磐石,上层生态百花齐放。Lombok 的"AST 改写"就是有人觉得"基础机制不够用"——但代价是绑定 javac 私有 API,反衬出 JDK 公共 API 的稳定性多么宝贵。
# 10.4 注解速查表
最后一张速查表——注解三态全览:
| 维度 | SOURCE | CLASS | RUNTIME |
|---|---|---|---|
| 写入 .class | ❌ | ✅ Invisible | ✅ Visible |
| JVM 加载 | ❌ | ❌ | ✅ |
| 反射可读 | ❌ | ❌ | ✅ |
| 典型代表 | @Override / @Data | @NonNull / JSR-305 | @Autowired / @Test |
| 消费者 | javac / APT | 静态分析工具 | 运行时框架 |
| 性能成本 | 0(编译完丢弃) | 0(运行时不读) | 反射成本(需缓存) |
注解处理三条路径:
| 路径 | 时机 | 能力边界 | 典型框架 |
|---|---|---|---|
| javac 内置检查 | 编译期·语义分析 | 仅检查不修改 | @Override / @Deprecated |
| APT 标准处理器 | 编译期·APT 阶段 | 仅生成新文件 | MapStruct / AutoService |
| APT + AST 改写 | 编译期·APT 阶段 | 修改已有 AST(私有 API) | Lombok |
| 运行时反射 | JVM 加载后 | 读注解 + 动态代理 | Spring / JPA / JUnit |
注解七条铁律:
1. 注解是带 ACC_ANNOTATION 的特殊接口,运行时表现为动态代理对象
2. @Retention 默认 CLASS,不是 RUNTIME——自定义注解必须显式声明
3. Retention 三态决定处理路径:编译器/工具/运行时框架
4. APT 标准 API 只能创建新文件,不能修改已有类
5. Lombok 通过私有 API 修改 javac AST 实现"无中生有"
6. @Inherited 仅传类继承,不传接口、不传方法——用 AnnotationUtils
7. 反射读注解必须缓存——ClassValue 是最佳容器
2
3
4
5
6
7
至此第 26 篇完成——我们用 1.5 万字把注解的字节码本质、元注解四件套、Retention 三态决定的三条处理路径、APT 标准能力、Lombok AST 黑魔法、运行时动态代理生成、Spring/JPA/JUnit/MapStruct 四大框架的注解使用模式一次讲透。卷三第三篇收官 ✅。
下一篇我们顺着"Java 语法糖背后的真相"这条线,进入卷三第 27 篇:Lambda与方法引用底层 ——把 invokedynamic 指令、LambdaMetafactory 动态生成代理类、与匿名内部类的字节码差异、捕获变量的"effectively final"约束一次讲透——揭开 () -> doSomething() 这一行代码背后 JVM 做了多少功夫,以及为什么 Java 8 用 invokedynamic 而不是直接生成 class。