编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • JVM内存模型与对象
      • 类加载与双亲委派
      • 垃圾回收与GC调优
      • 异常体系与JVM机制
      • 字节码指令集javap实战
      • JIT编译与去优化机制
      • JVM性能诊断工具链
      • OOM八大现场全景剖析
      • JVM参数调优全景图
      • GraalVM与AOT编译原理
      • HashMap底层哈希设计
      • String不可变与常量池
      • ArrayList与LinkedList源码
      • ConcurrentHashMap并发
      • TreeMap与红黑树原理
      • LinkedHashMap与LRU实现
      • Java数字类型原理
      • Object通用方法的契约
      • 泛型擦除与类型系统
      • 枚举原理与最佳实践
      • 注解原理与编译期处理
        • 1. 案例引入
          • 1.1 @Override 失效之谜
          • 1.2 Lombok 魔法失灵
          • 1.3 我们要回答什么
        • 2. 注解的本质
          • 2.1 注解即接口
          • 2.2 编译产物解析
          • 2.3 注解与注释的区别
          • 2.4 注解的三大用途
        • 3. 元注解全解
          • 3.1 @Retention 保留策略
          • 3.2 @Target 作用目标
          • 3.3 @Inherited 继承语义
          • 3.4 @Repeatable 重复注解
        • 4. 编译期处理 APT
          • 4.1 APT 工作原理
          • 4.2 注解处理器接口
          • 4.3 手写一个处理器
          • 4.4 多轮处理机制
        • 5. Lombok 字节码魔法
          • 5.1 Lombok 工作时机
          • 5.2 修改 AST 的黑魔法
          • 5.3 @Data 展开全貌
          • 5.4 Lombok 的争议
        • 6. 运行时反射读取
          • 6.1 AnnotatedElement 接口
          • 6.2 注解代理对象
          • 6.3 继承与组合注解
          • 6.4 性能优化策略
        • 7. Spring 注解体系
          • 7.1 组合注解原理
          • 7.2 @AliasFor 别名机制
          • 7.3 注解扫描与缓存
          • 7.4 注解驱动的 AOP
        • 8. 自定义注解实战
          • 8.1 参数校验注解
          • 8.2 权限控制注解
          • 8.3 缓存注解设计
          • 8.4 注解设计原则
        • 9. 实战陷阱清单
          • 9.1 Retention 选错
          • 9.2 注解不能继承
          • 9.3 反射读取性能坑
          • 9.4 注解处理器调试
        • 10. 综合案例串讲
          • 10.1 双案例真相揭晓
          • 10.2 一个注解的一生
          • 10.3 设计哲学回扣
          • 10.4 注解速查表
      • Lambda与引用底层原理
      • Stream原理与流水线设计
      • Optional设计原理
      • Record密封类与模式
      • 反射机制与动态代理
      • MethodHandle与VarHandle
      • 三大字节码框架对比
      • JavaAgent与Instrumentation机制
      • AOP三种实现路线对比
      • synchronized与锁升级
      • volatile与JMM内存模型
      • 线程池核心源码设计
      • Thread线程生命周期
      • AQS同步框架源码
      • 并发锁三剑客
      • CAS和Atomic深入分析
      • 五大同步器对比
      • CompletableFuture异步
      • IO模型演进BIO到AIO
      • ByteBuffer与堆外内存
      • 序列化原理与替代方案
      • 文件IO与NIO.2
      • 面向对象的真意
      • JDK设计模式上
      • JDK设计模式下
      • SPI与模块化设计
  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Java入门精通
  • 专栏博客
杨充
2026-06-02
目录

注解原理与编译期处理

# 21.注解原理与编译期处理

# 目录介绍

  • 1. 案例引入
    • 1.1 @Override 失效之谜
    • 1.2 Lombok 魔法失灵
    • 1.3 我们要回答什么
  • 2. 注解的本质
    • 2.1 注解即接口
    • 2.2 编译产物解析
    • 2.3 注解与注释的区别
    • 2.4 注解的三大用途
  • 3. 元注解全解
    • 3.1 @Retention 保留策略
    • 3.2 @Target 作用目标
    • 3.3 @Inherited 继承语义
    • 3.4 @Repeatable 重复注解
  • 4. 编译期处理 APT
    • 4.1 APT 工作原理
    • 4.2 注解处理器接口
    • 4.3 手写一个处理器
    • 4.4 多轮处理机制
  • 5. Lombok 字节码魔法
    • 5.1 Lombok 工作时机
    • 5.2 修改 AST 的黑魔法
    • 5.3 @Data 展开全貌
    • 5.4 Lombok 的争议
  • 6. 运行时反射读取
    • 6.1 AnnotatedElement 接口
    • 6.2 注解代理对象
    • 6.3 继承与组合注解
    • 6.4 性能优化策略
  • 7. Spring 注解体系
    • 7.1 组合注解原理
    • 7.2 @AliasFor 别名机制
    • 7.3 注解扫描与缓存
    • 7.4 注解驱动的 AOP
  • 8. 自定义注解实战
    • 8.1 参数校验注解
    • 8.2 权限控制注解
    • 8.3 缓存注解设计
    • 8.4 注解设计原则
  • 9. 实战陷阱清单
    • 9.1 Retention 选错
    • 9.2 注解不能继承
    • 9.3 数组属性陷阱
    • 9.4 注解处理顺序
  • 10. 综合案例串讲
    • 10.1 双案例真相揭晓
    • 10.2 一个注解的一生
    • 10.3 设计哲学回扣
    • 10.4 注解速查表

# 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);
    }
}
1
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);
    }
}
1
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
1
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>
1
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 是怎么组合多个注解的?
追问 ⑦:自定义注解怎么设计才不踩坑?
1
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(路由注册)
1
2
3
4
5
6
7
8
9

带着 7 个核心问题展开:

① 注解是什么类型?                              → 第2章
② @Retention 三种策略的本质区别?               → 第3章
③ APT 注解处理器怎么工作?                      → 第4章
④ Lombok 为什么能生成代码?                     → 第5章
⑤ 运行时注解怎么被读取?                        → 第6章
⑥ Spring 组合注解怎么实现?                     → 第7章
⑦ 自定义注解怎么设计不踩坑?                   → 第8、9章
1
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章)
1
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;
}
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();
}
1
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
}
1
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; }
}
1
2
3
4
5
6
7
8
9
10

结论:注解是接口,注解实例是动态代理对象——这是理解"运行时注解读取"的基础。

# 2.2 编译产物解析

注解信息如何存储在 class 文件中?

class 文件结构(第 13 篇已讲)中有两个专门的属性表:

RuntimeVisibleAnnotations    ← RUNTIME Retention 的注解
RuntimeInvisibleAnnotations  ← CLASS Retention 的注解(运行时不可见)
1
2

用 javap -verbose Foo.class 查看:

RuntimeVisibleAnnotations:
  0: #12(#13=s#14)
     com.example.MyAnnotation(
       value="hello"
     )
1
2
3
4
5

关键事实:

  • RUNTIME 注解 → 写入 RuntimeVisibleAnnotations → 运行时可通过反射读取
  • CLASS 注解 → 写入 RuntimeInvisibleAnnotations → 字节码里有,但反射读不到
  • SOURCE 注解 → 不写入 class 文件 → 编译后彻底消失

这就是 §1.1 中 @Override 的秘密:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)    // ★ SOURCE!编译后消失
public @interface Override { }
1
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 序列化映射
1
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    // 保留到运行时,可通过反射读取
}
1
2
3
4
5

三种策略的生命周期:

SOURCE:   源码 ──→ 编译 ──→ [消失]
CLASS:    源码 ──→ 编译 ──→ class文件 ──→ 类加载 ──→ [不可见]
RUNTIME:  源码 ──→ 编译 ──→ class文件 ──→ 类加载 ──→ 运行时可见
1
2
3

疑惑:CLASS 策略有什么用?既然运行时不可见,为什么不直接用 SOURCE?

论证——CLASS 策略的典型用途是字节码增强工具:

// 字节码增强工具(如 ASM、ByteBuddy)在类加载前读取 class 文件
// 它们不通过反射,而是直接解析字节码
// CLASS 注解对它们可见,对运行时反射不可见

// 典型例子:
@Retention(RetentionPolicy.CLASS)
public @interface NotNull { }    // 字节码分析工具用,运行时不需要

// 对比:
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull { }    // 运行时校验框架用(如 Hibernate Validator)
1
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+)
}
1
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")            // 对象创建
1
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(继承来的)
1
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!接口不传递
1
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() { }
1
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() { }
1
2
3
4
5
6

读取方式:

// 读取重复注解
Schedule[] schedules = method.getAnnotationsByType(Schedule.class);    // ★ JDK 8 新 API
// 或
Schedules container = method.getAnnotation(Schedules.class);
Schedule[] schedules2 = container.value();
1
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 文件
1
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 的修复方案)
1
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;
    }
}
1
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()        // 输出编译消息(警告/错误)
1
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());
        }
    }
}
1
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();
1
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
  处理器做收尾工作(如生成汇总文件)
1
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;
    }
    // 正常处理
}
1
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 文件
1
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);
1
2
3
4
5
6
7
8
9

# 5.2 修改 AST 的黑魔法

为什么说是"黑魔法":

  1. 使用了 JDK 内部 API(com.sun.tools.javac.*)——这些 API 在 JDK 9 模块化后被封装,需要 --add-opens 才能访问
  2. 违反了 APT 的设计契约——APT 设计为"只读已有代码,只写新文件"
  3. 不同 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+(虚拟线程相关适配)
1
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>
1
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 + ")";
    }
}
1
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)
1
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 可生成)
1
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;
    }
}
1
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
1
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)    // ★ 代理处理器
    );
}
1
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;
    }
}
1
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 { }
1
2

Spring 的解决方案——组合注解(§7.1 详解):

// 把多个注解组合成一个
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Controller
@ResponseBody
public @interface RestController {
    @AliasFor(annotation = Controller.class)
    String value() default "";
}
1
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);  // 有
1
2
3
4
5
6

# 6.4 性能优化策略

注解反射读取的性能开销来自两个地方:

  1. 反射本身的开销(第 07 篇已讲)——getAnnotation 内部有同步和类型检查
  2. 代理对象的 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() : "";
    });
}
1
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 { }
1
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 识别
1
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;
}
1
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 {};
    
    // ...
}
1
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 { }
1
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 传递过来)
1
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
   └── 后续请求直接查缓存,不再反射
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

关键优化——Spring 用 ASM 读字节码而不是反射加载类:

// Spring 内部:SimpleMetadataReader 用 ASM 读 class 文件
// 好处:不触发类初始化,不执行 static 块,速度快
// 坏处:只能读注解信息,不能执行代码
1
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()
   → 提交/回滚事务
1
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(),
        // ...
    );
}
1
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;
}
1
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() { ... }
}
1
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);
    }
}
1
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 中说明"谁来处理这个注解"
  否则使用方不知道注解是否生效
1
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  ★ 反射读不到!
1
2
3
4
5
6
7

§4.3 已分析——默认是 CLASS,不是 RUNTIME。

修复:

@Retention(RetentionPolicy.RUNTIME)   // ★ 必须显式声明
public @interface MyAnno { }
1
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 ✅
1
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);   // 第一次反射,之后命中缓存
1
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
1
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(...);
}
1
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;
}
1
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 的特殊枚举式接口
1
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 设计哲学回扣

跳出技术细节,提炼贯穿注解设计的三条工程哲学:

  1. 元数据应是语言一等公民:注解出现前,元数据散落在 javadoc 注释、XML 配置、命名约定(如 EJB 2.x 的"_Bean"后缀)中——没有类型安全、没有 IDE 支持、没有统一处理 API。注解把元数据提升为"语言关键字 + 类型系统的一部分"——让元数据具备和代码同等的工具友好性。这是 Java 5 的革命性设计——之后 Spring 从 XML 时代走向注解时代、Servlet 从 web.xml 走向 @WebServlet——整个 Java 生态因此更新换代。这条哲学在第 30 篇的 Record 和 Sealed 中再次闪光——把"不可变数据载体"和"封闭继承层级"也升级为语言一等公民。

  2. 三态 Retention 是关键解耦:SOURCE/CLASS/RUNTIME 看似只是三个枚举值——实际是注解世界的彻底解耦。SOURCE 只服务编译器(@Override/@SuppressWarnings)、CLASS 只服务静态分析工具(@NonNull)、RUNTIME 才进入运行时(@Autowired)——每个 Retention 对应一类消费者,互不打扰。这是 Java 设计中"分层语义"的典范——同一个机制(注解)通过一个开关(Retention)服务三类完全不同的工具链。对照来看——线程模型也有"用户态/内核态"、GC 也有"年轻代/老年代/元空间"——分层让复杂系统可治理。

  3. 机制开放,策略上交框架: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 是最佳容器
1
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。

上次更新: 2026/06/10, 11:13:41
枚举原理与最佳实践
Lambda与引用底层原理

← 枚举原理与最佳实践 Lambda与引用底层原理→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式