必须用RUNTIME的场景是运行时反射读取,如Spring依赖注入、JUnit测试扫描、AOP切面拦截;CLASS适用于字节码增强和构建工具处理;SOURCE仅用于编译期检查,零运行时开销。

注解不是“越持久越好”,而是要按实际读取时机选最轻量的策略。用错 @Retention 会拖慢启动、增大 class 体积、暴露元数据,甚至让反射读不到——性能问题往往就藏在这行元注解里。
什么时候必须用 RUNTIME
RUNTIME 是唯一支持反射读取的策略,但代价最高。只在以下场景才真正需要:
- 框架或业务逻辑在运行时动态解析注解:比如 Spring 的 @Autowired 在容器启动时注入依赖,@RequestMapping 在 DispatcherServlet 中匹配路径
- 测试框架执行阶段依赖:JUnit 的 @Test、@BeforeEach 需在运行时扫描并调用方法
- AOP 切面拦截:自定义 @LogExecutionTime 或 @Trace 注解,由代理对象在方法执行前后读取并记录
如果只是编译期校验或构建时处理,RUNTIME 就是冗余负担。
CLASS 策略适合字节码阶段协作
CLASS 是默认值,注解写进 class 文件但不进 JVM 内存,零反射开销。适合:
立即学习“Java免费学习笔记(深入)”;
- 构建时工具链处理:Dagger 生成依赖图、ASM 插入监控代码、Android 的资源编译器识别 @LayoutRes
- 内部 SDK 元数据标记:比如标注某个 API 是否已废弃,供 Gradle 插件生成兼容性报告,不希望 App 运行时感知
- 静态 AOP 实现:用字节码增强在编译后直接插入权限校验逻辑,避免运行时反射扫描
它比 RUNTIME 轻,又比 SOURCE 多一层中间态,是“编译即用、运行即弃”的理想选择。
SOURCE 策略是零成本选项
SOURCE 只存在于 .java 文件中,javac 编译完就彻底消失,不占 class 体积、不参与类加载、不触发任何运行时流程:
- 编译器提示类:自定义 @NonNull 配合 Checker Framework 做空值检查,或 IDE 识别的 @ApiStatus.Internal
- 静态分析标记:Lint 规则用的 @Keep、FindBugs 的 @SuppressFBWarnings
- 文档型注解:仅用于生成 Javadoc 或 IDE 自动补全,不参与任何执行逻辑
只要不需要反射,SOURCE 就该是首选。
性能代价必须量化看待
RUNTIME 的成本不是理论上的“轻微影响”:
- 每个 RUNTIME 注解平均增加 class 文件数十字节,百万级类规模下显著拉高包体积
- 类加载时 JVM 需解析并缓存注解元数据,实测在大型微服务中会抬升 GC 频率
- 高频反射调用(如每请求扫描 Controller 方法)比 CLASS 策略慢 3–5 倍,Spring Boot 2.6+ 已禁用默认反射扫描改用 index 加速启动
选高了浪费资源,选低了功能失效——关键就看“谁在哪个环节读它”。



















