runtime是唯一支持反射读取的策略,注解随类加载进入元空间;class保留在字节码但不加载到jvm,适用于编译后工具链;source仅存于源码,编译即丢弃,零运行时开销。

Java类加载过程中,Retention策略直接影响注解能否被加载、何时可见、以及是否参与运行时行为。它不是注解的“装饰”,而是决定注解在类加载生命周期中是否存在的一道开关——注解本身不执行逻辑,但它的保留级别决定了JVM是否把它当作元数据载入方法区(元空间),进而影响反射能否读取、框架能否生效、甚至应用启动速度。
RUNTIME:唯一能让反射真正“看见”注解的策略
RUNTIME是类加载阶段唯一能将注解信息带入JVM运行时环境的策略。当类被ClassLoader加载时,带有@Retention(RetentionPolicy.RUNTIME)的注解会作为类元数据的一部分,写入方法区(元空间),并由Class对象维护引用。
- 没有它,
clazz.getAnnotation(YourAnno.class)永远返回null,哪怕语法完全正确、编译通过 - SOURCE级注解编译后就消失,CLASS级注解虽存于.class文件,但JVM加载时不解析、不进内存,反射API根本触达不到
- Spring的
@Component、Jackson的@JsonProperty、JUnit的@Test都依赖这一机制——框架在类加载完成后扫描元数据,完成Bean注册或序列化配置
CLASS:字节码存在,但类加载时“视而不见”
CLASS策略让注解保留在.class文件的Attribute区域(如RuntimeVisibleAnnotations),但它被ClassLoader明确跳过——不解析、不构造Annotation对象、不放入运行时元数据结构。
- 适用于编译后、运行前的工具链:Lombok在javac阶段处理
@Data生成getter/setter;Dagger在APT阶段扫描@Inject生成Provider类 - 它不增加运行时开销,也不支持反射,但为构建流程提供结构化标记
- 若误将本该RUNTIME的注解设为CLASS,功能会静默失效:无编译错误、无运行异常,只有逻辑不触发
SOURCE:纯粹给编译器和IDE看的“便签纸”
SOURCE级注解连.class文件都不会出现,只存在于.java源码中。javac在词法/语法分析阶段读取并校验,随后彻底丢弃。
- 典型如
@Override——编译器检查父类是否有同名方法,校验通过即抹除,不占任何运行资源 - 适合团队内部开发提示:
@Todo、@Deprecated(forRemoval = true),或配合ErrorProne做静态规则检查 - 零体积影响、零类加载延迟、零GC压力,是最轻量也最“干净”的选择
默认值陷阱与性能代价必须正视
未显式声明@Retention的自定义注解,默认使用RetentionPolicy.CLASS。这个看似保守的默认值,常导致开发者误以为“反射能读到”,结果调试半天才发现注解根本不在运行时内存里。
- RUNTIME注解会使每个标注位置生成对应的Annotation实例,在类加载时占用元空间,并可能成为高频反射场景(如AOP拦截每请求方法)的性能瓶颈
- 大量RUNTIME注解会增大.class文件体积,延长类加载时间,尤其在微服务冷启动或热部署频繁的场景中影响明显
- 若仅需测试环境启用某能力,用SOURCE + 编译开关(如maven profile控制编译器参数)比RUNTIME + 运行时条件判断更彻底、更安全
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











