qinghuan
← Java SE / 全部笔记

单元 103 / Java SE

P2 · 按需深入L3 · 深入

注解如何生效:谁读取,何时读取

先找出谁在读取注解,再理解保留阶段、重复注解和编译期处理,避免以为加上标记就会自动生效。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能设计可重复注解并解释编译期生成与运行时增强为何是两条机制。

学习理由、准备要求与知识点

为什么学它

自定义规则与代码生成需要理解注解的保留阶段、容器和处理器配置。

先读这些内容

知识点清单

  • Target 与 Retention
  • 默认 CLASS 策略
  • Documented 用途
  • Inherited 类级限制
  • Repeatable 容器
  • 按类型读取重复注解
  • 编译期注解处理
  • 处理器与运行时 AOP 区分

运行前先确认Java 17;源码在本机 Microsoft OpenJDK 17.0.20.1 src.zip 核验,在线链接为上游 jdk-17.0.20-ga。深入模块可在掌握基础后回读。

01先弄懂它是什么

写了 @Limit,为什么程序仍然没有检查数值?因为注解只是附在代码上的说明数据,本身不会执行校验、记录日志或生成代码。必须有框架、反射代码或编译期处理器读取它,再决定做什么。设计注解时,先明确谁来读、什么时候读,以及没读到时怎么办。

@Target 规定注解能写在哪里,例如类、方法、参数或类型使用处;@Retention 规定它能保留到哪个阶段。SOURCE 编译后不保留;CLASS 留在类文件中,但通常不能通过运行时反射读取;RUNTIME 才提供运行时读取条件。没写 Retention 时默认 CLASS。@Documented 只影响文档展示,不会让规则自动执行。

重复注解适合表达多条同类规则,例如一个配置同时要求数值至少十且至多二十。@Repeatable 指向容器注解,其 value 返回该注解数组,编译器按照规则组织存储。容器必须满足保留期、目标和继承等兼容要求。读取端应优先考虑 getAnnotationsByType,以免误以为一个位置只能找到一个实例。

文中这几个词,先认一认

保留策略
像决定一张标签要留到哪一步:SOURCE 只留在源码,CLASS 会写进 class 文件,RUNTIME 还允许运行时反射读取。写进 class 文件不自动等于运行时反射能读到,选哪一种要看谁在什么阶段读取。
重复注解容器
同一位置写了多个同类注解时,用一个带数组的注解保存它们。
注解处理器
编译时读取代码中的注解等信息,发出检查结果或生成文件的程序组件。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

@Inherited 仅影响类声明注解沿超类链的某些反射查询,不会把方法注解自动继承到覆盖方法,也不通过实现接口获得注解。getDeclaredAnnotationsByType 只关注直接或容器间接声明;getAnnotationsByType 还按继承规则处理关联注解。不能把 Java 方法覆盖与注解继承混为一谈,尤其是代理类和接口式框架需要自己定义合并策略。

编译期处理器通过 javax.annotation.processing 和 javax.lang.model 查看类、方法、注解等声明。处理可能分几轮进行:这一轮生成的源文件,下一轮还可能被继续处理;processingOver 表示处理已经结束。

因此用 Filer 生成文件时,要记住哪些已经生成,不能每一轮都创建同一路径。标准注解处理适合检查声明、生成辅助类;它不是任意修改原代码结构的通用入口。编译器内部把代码表示成树,常称 AST,对这棵树的任意改写并不属于标准处理接口的承诺。

Java 17 项目可以明确配置 -processor、-processorpath 等参数,让编译器从指定位置加载处理器;处理器自己的依赖也必须位于正确路径。较新 JDK 的默认发现策略可能变化,因此不要把本篇命令无条件套用到其他版本。运行期 AOP 则发生在程序调用阶段,它可以观察参数与异常,编译处理器通常无法知道未来每次请求的实际值。

本篇把重复 @Limit 解释为多个整数闭区间的交集。读取元数据后仍需显式执行聚合算法,才能得到有效边界;若交集为空,在构建配置时就报错。将检查前移到启动或构建阶段,有助于把错误定位到规则声明,而不是直到用户请求时才发现互相矛盾的条件。

正在加载流程图…

图展示 RUNTIME 注解的消费路径;编译期处理是另一条入口,不会由反射自动触发。
查看流程图源码(Mermaid)
flowchart TD
 A["源码注解声明"] --> B["按 Retention 编译保留"]
 B --> C["运行时按类型展开容器"]
 C --> D["显式计算规则交集"]
 D --> E["有效区间或配置错误"]

按顺序看,每一步做了什么

  1. 确定消费者位于编译阶段还是运行阶段。
  2. 设置 Target、Retention 与容器兼容策略。
  3. 按类型读取多个注解并明确继承范围。
  4. 校验规则自身合法性。
  5. 聚合约束并对冲突给出诊断。

03对照实现代码

Class.getAnnotationsByType:关联注解的入口 (新标签页)↗

Microsoft OpenJDK 17.0.20.1;java.base/java/lang/Class.java · java.lang.Class#getAnnotationsByType(Class)

以下为原创 Java 17 教学实现,不是原样 JDK 源码;只保留文中明确讨论的机制,省略优化、兼容及内部接口。

核心机制代码 · java
import java.lang.annotation.*;
import java.util.*;
public final class SourceModel {
  @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE)
  @Repeatable(Tags.class) public @interface Tag { String value(); }
  @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE)
  public @interface Tags { Tag[] value(); }
  static List<String> declared(Class<?> type) {
    // 原创消费端模型:仅查询本类,不模拟整个继承合并算法。
    Tag[] tags=type.getDeclaredAnnotationsByType(Tag.class);
    List<String> out=new ArrayList<>();
    for(Tag tag:tags) out.add(tag.value()); // 展开后的注解仍需业务代码解释。
    return List.copyOf(out); // 输出不可修改快照,避免调用者改动目录。
  }
}

本机 Class.getAnnotationsByType 先检查注解类型非空,获取 annotationData,再委托 AnnotationSupport.getAssociatedAnnotations。这里的 associated 意味着结果可能涉及重复容器和指定继承规则,而不是简单从 Map 按键取一个对象。类文件的解析与缓存管理位于更底层,不由这个公开入口独自完成。

getDeclaredAnnotationsByType 是相近但不同的选择。教学模型用它明确限定当前类上的标签,避免让读者误以为扫描器应当默认沿任意继承关系收集所有内容。真正需要继承合并时,要写出覆盖、追加、去重及冲突的业务规则,再选择合适的反射入口。

教学代码是注解消费端的原创完整类,使用公开 API 展示容器展开后的结果,没有重写 JDK 的字节码注解解析器。注解实例的内部表示、异常代理和缓存失效等细节并未建模,因此不能据此推导反射在所有加载环境下的性能或实现结构。

读取注解也可能失败,例如注解中引用的类型已经不可用、枚举常量发生不兼容变化,或类文件中结构与声明不符。库升级时不仅检查业务接口,还要检查运行期元数据消费者依赖了哪些成员。适当在启动阶段读取并验证,可以让兼容错误更早暴露。

代码先调用谁,再调用谁

  1. Class.getAnnotationsByType
  2. annotationData
  3. AnnotationSupport.getAssociatedAnnotations
  4. 展开重复容器并按继承规则关联
  5. 调用方解释注解属性

04动手写一个例子

重复区间规则的交集验证

读取类上的重复 @Limit,求所有整数闭区间交集;没有注解时不附加限制,非法单条或空交集抛异常。完整 Java 17 Main 自测不需要处理器或框架。

输入 1
[0,20] 与 [10,30]
输出 1
[10,20]
输入 2
[0,1] 与 [2,3]
输出 2
配置冲突

交集中的数必须同时满足每条下界,所以有效下界是所有 min 的最大值;上界则是所有 max 的最小值。初始采用整个 int 范围,代表没有规则时不附加约束。每处理一条注解,就收缩当前区间,绝不会放宽已有要求。

先检查单条 min 不大于 max,再合并并检查总下界是否超过上界。单条错误和组合冲突是不同诊断,有助于定位错误来源。算法只使用比较与赋值,不对边界加一,因此 MIN_VALUE、MAX_VALUE 等极值不会触发算术溢出。

重复注解与显式容器都应产生相同的规则数组。自测同时覆盖两种写法,再验证相交、单点、未声明、冲突和非法规则。这里没有 @Inherited,子类没有直接声明时按无限制处理;如果改动继承策略,就必须同步更新 API 读取方式与测试期望。

为什么这样写是对的处理前 k 条规则后的闭区间恰好是这 k 条规则允许值的交集。

时间 O(r),r 为展开后的规则数。额外空间 聚合状态 O(1);反射返回数组 O(r)。

每条规则只检查两次边界并更新两个整数,不枚举区间中的数,因此与区间跨度无关。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 37 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
import java.lang.annotation.*;
import java.util.*;
public class Main {
  @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Repeatable(Limits.class)
  public @interface Limit { int min(); int max(); }
  @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE)
  public @interface Limits { Limit[] value(); }
  record Bounds(int min,int max) {}
  @Limit(min=0,max=20) @Limit(min=10,max=30) static class Overlap {}
  @Limit(min=2,max=2) static class Point {}
  static class Empty {}
  @Limit(min=0,max=1) @Limit(min=2,max=3) static class Conflict {}
  @Limit(min=4,max=2) static class Invalid {}
  @Limits({@Limit(min=-2,max=5),@Limit(min=0,max=3)}) static class Container {}
  static Bounds bounds(Class<?> type) {
    int lo=Integer.MIN_VALUE, hi=Integer.MAX_VALUE; // 空规则表示整个整数域。
    for(Limit rule:type.getAnnotationsByType(Limit.class)) {
      if(rule.min()>rule.max()) throw new IllegalArgumentException("invalid rule");
      lo=Math.max(lo,rule.min()); hi=Math.min(hi,rule.max()); // 交集只能收缩。
      if(lo>hi) throw new IllegalArgumentException("conflicting rules"); // 比较边界,不做加一。
    }
    return new Bounds(lo,hi);
  }
  static void eq(Object a,Object b) { if(!Objects.equals(a,b)) throw new AssertionError(a+" != "+b); }
  static void fails(Runnable r) {
    try {r.run();} catch(IllegalArgumentException e) {return;}
    throw new AssertionError("expected invalid configuration");
  }
  public static void main(String[] args) {
    eq(bounds(Overlap.class),new Bounds(10,20));
    eq(bounds(Point.class),new Bounds(2,2));
    eq(bounds(Empty.class),new Bounds(Integer.MIN_VALUE,Integer.MAX_VALUE));
    fails(()->bounds(Conflict.class)); fails(()->bounds(Invalid.class));
    eq(bounds(Container.class),new Bounds(0,3)); // 显式容器与重复写法等价地展开。
    System.out.println("PASS 6");
  }
}

这些特殊情况也要试一试

  • 空规则采用整个 int 范围。
  • 闭区间交于一点仍合法。
  • 单条反转与组合空交集分别报错。
进阶选读 · 编译阶段发现互斥规则

场景希望在交付类文件之前阻止无效配置。

做法用 AbstractProcessor 读取注解镜像,在每个声明内聚合边界,以 Messager 给出带元素位置的错误;处理生成轮次和重复访问。

复杂度规则扫描 O(r),整体成本另含编译器模型与生成文件。

好处与代价能提前发现静态错误,但不能代替运行时用户输入校验;处理器需要独立构建与兼容验证。

05项目里怎么用

注解适合附着在代码结构上的相对稳定配置。经常由运营修改的阈值更适合外部配置或数据管理,避免每次改数值都重新编译发布。把注解用于框架扩展时,应明确默认值、是否允许重复以及多个来源的优先级,并让错误信息包含声明位置和冲突规则。

可以在启动时扫描注解,整理成不可修改的规则目录,减少每次请求重复反射。但若缓存键保存 Class,就要考虑旧类何时能释放;热重载或插件系统不能无限保留旧类。扫描并缓存了注解,也不表示业务输入必然经过校验,绕过执行校验的入口仍会跳过规则。

编译处理器是构建供应链的一部分,应固定版本、明确依赖与执行入口,并在干净构建里验证生成结果可重复。生成代码是否进入版本控制由团队约定,但不能依赖某位开发者机器上残留的生成文件才能成功。处理器与运行时框架各自的测试范围应独立记录,避免只验证其中一条路径。

容易踩的坑

  • 忘记 RUNTIME 导致反射读不到声明。
  • 认为 @Inherited 会继承接口或覆盖方法上的规则。
  • 只调用 getAnnotation 而漏掉重复容器。
  • 处理器每轮生成同一个文件并触发 Filer 错误。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
运行时反射规则部署时可统一扫描与诊断需要保留注解并支付扫描成本小型框架扩展
编译期处理尽早失败且可生成辅助代码构建配置和版本兼容更复杂静态代码生成
外部配置无需改源代码即可调整参数要管理校验、版本及发布频繁变化的业务阈值

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01默认保留策略是什么?

展开参考答案与推导

未声明 Retention 时默认 CLASS,通常进入类文件但运行时反射无法按 RUNTIME 注解读取。需要先确定消费者所在阶段,再选择保留策略。

02有注解是否就会执行规则?

展开参考答案与推导

不会。注解只是元数据,必须由处理器、反射逻辑或框架读取并解释。没有消费者的注解不会自动拦截方法或验证输入。

03重复注解为何需要容器?

展开参考答案与推导

容器提供多个同类型声明的类文件表示,value 成员承载注解数组。按类型读取 API 能按规则展开它,调用方不必把重复声明误当成单实例。

换个情况,还会处理吗

01覆盖方法后旧注解不见了怎么办?

展开参考答案与推导

先检查框架是否自己沿父方法或接口合并,Java 的 @Inherited 本身不负责方法继承。明确合并规则后再实现扫描,不能简单给注解加 Inherited 就期待修复。

02本地编译成功而干净构建缺类怎么办?

展开参考答案与推导

检查生成源文件是否来自残留目录,处理器是否在正确路径且被明确启用,并观察处理轮次和诊断。干净构建应从声明和固定依赖重新生成全部必要文件。

03规则冲突应在请求时还是启动时检查?

展开参考答案与推导

静态注解冲突通常可在启动或编译时提前阻止,避免错误配置进入服务状态;每次请求的动态输入仍需运行时验证,两类检查可以同时存在。

写代码试一试

01计算三个闭区间交集。

输入 / 条件
[1,9]、[3,7]、[7,10]
输出 / 预期
[7,7]

提示 最大下界与最小上界。

展开参考答案与推导

下界依次为一、三、七,上界依次为九、七、七,最终单点合法。只有下界严格大于上界时才为空。

02如何查本类重复标签?

输入 / 条件
类上两个 @Tag
输出 / 预期
长度为 2 的数组

提示 限定声明范围。

展开参考答案与推导

调用 getDeclaredAnnotationsByType(Tag.class),即可展开直接或容器间接声明;它不会按超类继承规则补充未声明标签。

03如何让无规则时避免溢出?

输入 / 条件
没有 Limit
输出 / 预期
整个 int 范围

提示 不要计算跨度。

展开参考答案与推导

初始化下界为 MIN_VALUE、上界为 MAX_VALUE,仅比较和赋值,不做 max-min 或边界加一。结果表示允许范围,无需枚举其中所有整数。

读完之后,独立完成

课后作业:自己做一遍

本篇共 8 道作业,先完成 6 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能设计可重复注解并解释编译期生成与运行时增强为何是两条机制。

本篇重点加练

重点加练注解继承和读取方式,检验发现的元数据是否符合实际声明来源。

  • Inherited 类级限制
  • 按类型读取重复注解

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01代码推演

    定义三种不同 Retention 的注解,固定 Target 后分别在源码、编译产物和运行时反射中检查;另说明 @Documented 需要通过哪种产物验证。

    提交内容
    最小注解、检查命令与观察表。
    怎样算完成
    • 保留阶段与使用位置分开
    • 文档表现不冒充运行时行为

    练到的知识点Target 与 Retention · 默认 CLASS 策略 · Documented 用途

  2. 作业 02排错实验

    给父类、接口和方法分别添加可继承性相关注解,在子类覆盖方法与未覆盖方法两组情况下读取,注明使用 getAnnotation 还是声明级读取。

    提交内容
    Java 实验、自写继承边界测试及运行记录。
    怎样算完成
    • 类继承与接口实现分别验证
    • 读取 API 选择可追踪

    练到的知识点Inherited 类级限制

综合应用

  1. 作业 03编程实践

    设计可重复 @Tag 注解与容器,读取类上的所有标签并按明确政策检查重复名称;覆盖零个、一个、多个及显式容器写法。

    提交内容
    Java 实现、自写重复注解边界测试和运行证据。
    怎样算完成
    • 不同声明形式得到一致合同
    • 读取顺序不做无依据假设

    练到的知识点Repeatable 容器 · 按类型读取重复注解

  2. 作业 04原理问答

    某注解生成构建代码,另一注解运行时触发审计。画出两者生效阶段、执行者与产物,说明关闭处理器和绕过代理分别需要怎样的证据。

    提交内容
    两条执行链与诊断清单。
    怎样算完成
    • 编译期生成与运行时拦截区分
    • 注解本身不等于执行者

    练到的知识点编译期注解处理 · 处理器与运行时 AOP 区分

  3. 作业 05面试场景

    本机增量构建正常,清理输出目录后找不到生成类;运行时又读不到另一个注解。分别建立最短排查路径,避免把两类问题归为同一个注解失效。

    提交内容
    构建链与反射链的排查树。
    怎样算完成
    • 检查实际生成产物和保留策略
    • 依赖版本与编译入口可核验

    练到的知识点Target 与 Retention · 默认 CLASS 策略 · 按类型读取重复注解 · 编译期注解处理

  4. 作业 06排错实验

    构造父类、子类和接口,分别直接标注可重复注解及显式容器;对比声明级读取与按类型读取,记录注解来自哪个声明位置。

    提交内容
    Java 实验、自写继承/覆盖/空声明测试与运行证据。
    怎样算完成
    • 读取方法逐一记录
    • 来源判断由实际声明支持

    练到的知识点Inherited 类级限制 · 按类型读取重复注解

进阶挑战 · 掌握前置后选做

  1. 作业 07编程实践

    完成反射基础后:实现启动期规则目录,读取重复的数值范围注解,报告冲突、非法范围和缺少规则;仅做元数据验证,不动态执行任意用户方法。

    提交内容
    Java 实现、自写冲突/继承/无规则测试与运行证据。
    怎样算完成
    • 注册时失败可定位声明
    • 扫描范围和执行权限清楚

    练到的知识点Target 与 Retention · Repeatable 容器 · 按类型读取重复注解 · 处理器与运行时 AOP 区分

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 08思维拓展 · 条件变化

    业务要求“子类标签追加到父类标签之后”而现有反射读取直接作为最终规则。检查这是否等同 API 的默认行为,并设计排序、重复与覆盖的业务合同。

    提交内容
    反射结果与业务规则对照、验收样本。
    怎样算完成
    • 元数据发现与规则合成分开
    • 结论有具体继承样例

    练到的知识点Inherited 类级限制 · 按类型读取重复注解

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗