读完这篇,你能做什么
能设计可重复注解并解释编译期生成与运行时增强为何是两条机制。
学习理由、准备要求与知识点
为什么学它
自定义规则与代码生成需要理解注解的保留阶段、容器和处理器配置。
先读这些内容
知识点清单
- 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 解释为多个整数闭区间的交集。读取元数据后仍需显式执行聚合算法,才能得到有效边界;若交集为空,在构建配置时就报错。将检查前移到启动或构建阶段,有助于把错误定位到规则声明,而不是直到用户请求时才发现互相矛盾的条件。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["源码注解声明"] --> B["按 Retention 编译保留"]
B --> C["运行时按类型展开容器"]
C --> D["显式计算规则交集"]
D --> E["有效区间或配置错误"]按顺序看,每一步做了什么
- 确定消费者位于编译阶段还是运行阶段。
- 设置 Target、Retention 与容器兼容策略。
- 按类型读取多个注解并明确继承范围。
- 校验规则自身合法性。
- 聚合约束并对冲突给出诊断。
03对照实现代码
Microsoft OpenJDK 17.0.20.1;java.base/java/lang/Class.java · java.lang.Class#getAnnotationsByType(Class)
以下为原创 Java 17 教学实现,不是原样 JDK 源码;只保留文中明确讨论的机制,省略优化、兼容及内部接口。
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 的字节码注解解析器。注解实例的内部表示、异常代理和缓存失效等细节并未建模,因此不能据此推导反射在所有加载环境下的性能或实现结构。
读取注解也可能失败,例如注解中引用的类型已经不可用、枚举常量发生不兼容变化,或类文件中结构与声明不符。库升级时不仅检查业务接口,还要检查运行期元数据消费者依赖了哪些成员。适当在启动阶段读取并验证,可以让兼容错误更早暴露。
代码先调用谁,再调用谁
Class.getAnnotationsByTypeannotationDataAnnotationSupport.getAssociatedAnnotations- 展开重复容器并按继承规则关联
- 调用方解释注解属性
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 条规则允许值的交集。
每条规则只检查两次边界并更新两个整数,不枚举区间中的数,因此与区间跨度无关。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
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 类级限制
- 按类型读取重复注解
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01代码推演
定义三种不同 Retention 的注解,固定 Target 后分别在源码、编译产物和运行时反射中检查;另说明 @Documented 需要通过哪种产物验证。
- 提交内容
- 最小注解、检查命令与观察表。
- 怎样算完成
- 保留阶段与使用位置分开
- 文档表现不冒充运行时行为
练到的知识点Target 与 Retention · 默认 CLASS 策略 · Documented 用途
- 作业 02排错实验
给父类、接口和方法分别添加可继承性相关注解,在子类覆盖方法与未覆盖方法两组情况下读取,注明使用
getAnnotation还是声明级读取。- 提交内容
- Java 实验、自写继承边界测试及运行记录。
- 怎样算完成
- 类继承与接口实现分别验证
- 读取 API 选择可追踪
练到的知识点Inherited 类级限制
综合应用
- 作业 03编程实践
设计可重复 @Tag 注解与容器,读取类上的所有标签并按明确政策检查重复名称;覆盖零个、一个、多个及显式容器写法。
- 提交内容
- Java 实现、自写重复注解边界测试和运行证据。
- 怎样算完成
- 不同声明形式得到一致合同
- 读取顺序不做无依据假设
练到的知识点Repeatable 容器 · 按类型读取重复注解
- 作业 04原理问答
某注解生成构建代码,另一注解运行时触发审计。画出两者生效阶段、执行者与产物,说明关闭处理器和绕过代理分别需要怎样的证据。
- 提交内容
- 两条执行链与诊断清单。
- 怎样算完成
- 编译期生成与运行时拦截区分
- 注解本身不等于执行者
练到的知识点编译期注解处理 · 处理器与运行时 AOP 区分
- 作业 05面试场景
本机增量构建正常,清理输出目录后找不到生成类;运行时又读不到另一个注解。分别建立最短排查路径,避免把两类问题归为同一个注解失效。
- 提交内容
- 构建链与反射链的排查树。
- 怎样算完成
- 检查实际生成产物和保留策略
- 依赖版本与编译入口可核验
练到的知识点Target 与 Retention · 默认 CLASS 策略 · 按类型读取重复注解 · 编译期注解处理
- 作业 06排错实验
构造父类、子类和接口,分别直接标注可重复注解及显式容器;对比声明级读取与按类型读取,记录注解来自哪个声明位置。
- 提交内容
- Java 实验、自写继承/覆盖/空声明测试与运行证据。
- 怎样算完成
- 读取方法逐一记录
- 来源判断由实际声明支持
练到的知识点Inherited 类级限制 · 按类型读取重复注解
进阶挑战 · 掌握前置后选做
- 作业 07编程实践
完成反射基础后:实现启动期规则目录,读取重复的数值范围注解,报告冲突、非法范围和缺少规则;仅做元数据验证,不动态执行任意用户方法。
- 提交内容
- Java 实现、自写冲突/继承/无规则测试与运行证据。
- 怎样算完成
- 注册时失败可定位声明
- 扫描范围和执行权限清楚
练到的知识点Target 与 Retention · Repeatable 容器 · 按类型读取重复注解 · 处理器与运行时 AOP 区分
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 08思维拓展 · 条件变化
业务要求“子类标签追加到父类标签之后”而现有反射读取直接作为最终规则。检查这是否等同 API 的默认行为,并设计排序、重复与覆盖的业务合同。
- 提交内容
- 反射结果与业务规则对照、验收样本。
- 怎样算完成
- 元数据发现与规则合成分开
- 结论有具体继承样例
练到的知识点Inherited 类级限制 · 按类型读取重复注解
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读教程英文
Dev.java:注解 (新标签页)↗
阅读声明注解、预定义注解与重复注解,区分元数据和处理器。
- 实践参考文档英文
Java 17 API:Retention (新标签页)↗
确认默认 CLASS 保留策略以及 RUNTIME 对反射读取的作用。
- 深入参考文档英文
Java 17 API:Repeatable (新标签页)↗
阅读容器注解类型要求,理解多个同类型注解如何存储。
- 深入参考文档英文
Java 17:javac 命令 (新标签页)↗
查阅 -encoding、-d、--release 和 classpath,用于解释编译器实际输入与输出。
- 深入参考文档英文
OpenJDK 17u java.lang.Class (新标签页)↗
核对注解类型检查、
annotationData与关联注解委托位置。 - 深入参考文档英文
Java 17 Class (新标签页)↗
比较
getAnnotationsByType与getDeclaredAnnotationsByType。