news 2026/5/16 13:15:26

关于注解(Annotation)的详细介绍

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
关于注解(Annotation)的详细介绍

目录

1、Java注解

1.1、介绍

1.2、注解的元注解

1.3、高级特性

1.4、框架中的典型应用

1.5、自定义注解

2、注解原理

2.1、注解如何存储

2.2、JVM 加载阶段

2.3、反射读取原理

2.4、default的实现机制

3、生命周期阶段

3.1、生命周期

3.2、保留策略

4、注意事项


前沿

注解(Annotation)的未来:

一、历史回溯:注解的三次进化浪潮

1.1 第一代:编译期辅助(2004–2010)

  • 代表:@Override,@SuppressWarnings

  • 特点:仅作用于编译器,无运行时语义

  • 局限:无法参与程序逻辑

1.2 第二代:运行时反射驱动(2010–2020)

  • 代表:Spring @Component,JPA @Entity

  • 核心:@Retention(RUNTIME) + 反射

  • 问题

    • 启动慢(大量反射解析)

    • 内存占用高(代理对象)

    • 无法用于 GraalVM Native Image

1.3 第三代:编译时代码生成(2020–至今)

  • 代表:Lombok, MapStruct, Micronaut

  • 突破:@Retention(SOURCE) +注解处理器(Annotation Processor)

  • 优势

    • 零运行时开销

    • 兼容 AOT 编译

    • 类型安全

趋势“运行时注解 → 编译时注解” 是云原生时代的必然选择。


1、Java注解

1.1、介绍

元数据的载体,它本身不执行任何操作,但可以被工具、编译器、框架读取并触发相应行为。

注解 = 不改变程序逻辑的“标记”

示例:常见注解

@Override // 编译期检查:是否重写了父类方法 @Deprecated // 标记过时方法 @SuppressWarnings("unchecked") // 抑制编译警告 @RestController // Spring:标识这是一个 REST 控制器

编译器生成的特殊接口

  • 注解在编译后是一个继承自java.lang.annotation.Annotation的接口
  • 所有注解成员方法 = 接口中的抽象方法
  • 使用注解的地方 = 生成一个匿名内部类实现该接口

1.2、注解的元注解

元注解(Meta-Annotations) =用来注解其他注解的注解。

Java 提供了 5 个:

元注解作用
@Retention指定注解生命周期(SOURCE/CLASS/RUNTIME)
@Target指定注解可用的位置(类、方法、字段等)
@Documented是否包含在 JavaDoc 中
@Inherited是否可被子类继承(仅对类有效)
@Repeatable允许同一位置重复使用注解(Java 8+)

示例:@Target 可选值

@Target({ ElementType.TYPE, // 类、接口、枚举 ElementType.METHOD, // 方法 ElementType.FIELD, // 字段 ElementType.PARAMETER, // 参数 ElementType.CONSTRUCTOR, // 构造函数 ElementType.LOCAL_VARIABLE, // 局部变量 ElementType.ANNOTATION_TYPE // 注解类型 })

1.3、高级特性

1.@Inherited —— 注解继承

限制:仅对类上的注解有效,且只继承一级

@Inherited @Retention(RetentionPolicy.RUNTIME) @interface ParentAnno {} @ParentAnno class Parent {} class Child extends Parent {} // Child 自动拥有 @ParentAnno

⚠️ 注意:只对类继承有效,接口/方法/字段不继承

2. @Repeatable —— 重复注解(Java 8+)

@Retention(RetentionPolicy.RUNTIME) @Repeatable(Authors.class) public @interface Author { String name(); } @Retention(RetentionPolicy.RUNTIME) public @interface Authors { Author[] value(); } // 使用 @Author(name = "Alice") @Author(name = "Bob") public class Book {}

实际字节码:

@Authors({ @Author(name = "Alice"), @Author(name = "Bob") }) class Book {}

JVM 如何知道要“展开”?

  • @Author 注解上标记了 @Repeatable(Authors.class)

  • 反射 API 特殊处理:

// getAnnotationsByType(Author.class) 会自动展开 Author[] authors = Book.class.getAnnotationsByAssistant(Author.class);

3. 注解成员类型限制

注解的成员只能是以下类型:

  • 基本类型(int,boolean 等)
  • String
  • Class
  • 枚举
  • 其他注解
  • 以上类型的数组

不能是:Object, List, 自定义对象等

1.4、框架中的典型应用

框架注解示例保留策略作用
Spring@Component,@AutowiredRUNTIME依赖注入、Bean 管理
JPA@Entity,@ColumnRUNTIMEORM 映射
JUnit@Test,@BeforeEachRUNTIME单元测试
Lombok@Data,@BuilderSOURCE编译时生成代码
Swagger@ApiOperationRUNTIMEAPI 文档生成

1.5、自定义注解

自定义注解接口:

// 源码 @Retention(RetentionPolicy.RUNTIME) public @interface MyAnno { String value() default "hello"; int count() default 0; }

编译后(等价 Java 代码)

// MyAnno.class 反编译后 public interface MyAnno extends Annotation { String value(); // 注意:没有 default! int count(); }

为什么没有 default?默认值由注解使用处的字节码反射 API在运行时提供,不在接口中。

步骤 1:定义注解(使用 @interface)

import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) // 可用在类和方法上 @Documented // 注解会包含在 JavaDoc 中 public @interface ApiVersion { String value() default "1.0"; // 成员变量(带默认值) boolean deprecated() default false; }

步骤 2:使用注解

@ApiVersion(value = "2.0", deprecated = true) public class UserService { @ApiVersion("3.0") public void createUser() { ... } }

步骤 3:通过反射读取(运行时)

// 读取类上的注解 ApiVersion classAnno = UserService.class.getAnnotation(ApiVersion.class); System.out.println(classAnno.value()); // 输出: 2.0 // 读取方法上的注解 Method method = UserService.class.getMethod("createUser"); ApiVersion methodAnno = method.getAnnotation(ApiVersion.class); System.out.println(methodAnno.value()); // 输出: 3.0

2、注解原理

2.1、注解如何存储

Java 编译器(javac)会将注解信息写入 .class 文件的属性表(Attributes)中。

1. 类/方法/字段上的注解

存在对应结构的 RuntimeVisibleAnnotations 属性中

示例:带注解的类

@MyAnno(value = "test", count = 5) public class Demo {}

字节码结构(简化)

ClassFile { ... attributes_count: 2 attributes[0]: SourceFile attributes[1]: RuntimeVisibleAnnotations { ← 关键! num_annotations: 1 annotation { type_index: #MyAnno // 指向常量池 num_element_value_pairs: 2 element_value_pairs[0]: { name="value", value="test" } element_value_pairs[1]: { name="count", value=5 } } } }

2. 保留策略决定是否写入字节码

@Retention是否写入.class属性名
SOURCE不写入
CLASS写入RuntimeInvisibleAnnotations
RUNTIME写入RuntimeVisibleAnnotations

JVM 规范规定

  • RuntimeVisibleAnnotations → 运行时可通过反射访问
  • RuntimeInvisibleAnnotations → 仅工具可读(如 ASM)

2.2、JVM 加载阶段

注解如何进入内存?当 JVM 加载 .class 文件时:

步骤 1:解析常量池

  • 将 MyAnno 的全限定名存入运行时常量池

步骤 2:创建 java.lang.Class对象

  • 如果注解是 RUNTIME 策略,JVM 会:

    1. 解析 RuntimeVisibleAnnotations 属性

    2. 不立即创建注解实例,而是保存原始数据(name-value pairs)

    3. 将这些数据关联到 Class/Method/Field 对象的内部字段

关键源码(OpenJDK):

// java.lang.Class private volatile AnnotationData annotationData; // 注解数据懒加载 private AnnotationData getAnnotationData() { if (annotationData == null) { // 从 JVMTI 或字节码解析注解 annotationData = createAnnotationData(); } return annotationData; }

💡注解是懒加载的:只有第一次调用 getAnnotation() 时才解析!

2.3、反射读取原理

当你调用:

MyAnno anno = clazz.getAnnotation(MyAnno.class);

JVM 并不会返回一个真实的MyAnno实例,而是返回一个动态代理对象

内部实现(简化版)

// sun.reflect.annotation.AnnotationParser static <A extends Annotation> A annotationForMap( final Class<A> type, final Map<String, Object> memberValues ) { return (A) Proxy.newProxyInstance( type.getClassLoader(), new Class<?>[] { type }, new InvocationHandler() { public Object invoke(Object proxy, Method method, Object[] args) { // 1. 如果是 annotationType(),返回 type if (method.getName().equals("annotationType")) return type; // 2. 否则从 memberValues 中取值 String member = method.getName(); Object value = memberValues.get(member); // 3. 处理默认值(如果 value 为 null) if (value == null) { value = method.getDefaultValue(); // ← 关键! } return value; } } ); }

为什么用代理?

  1. 节省内存:不需要为每个注解创建真实对象

  2. 支持默认值:通过method.getDefaultValue() 动态填充

  3. 类型安全:代理实现了目标注解接口

2.4、default的实现机制

问题:默认值存在哪里?

  • 不在注解接口中

  • 不在使用处的字节码中

  • 而是在注解定义的.class文件的AnnotationDefault属性中

示例:

public @interface MyAnno { String value() default "hello"; // 默认值 "hello" }

字节码中的 AnnotationDefault

Method: value() attributes[0]: AnnotationDefault { default_value: "hello" }

反射读取时如何获取默认值?

// 当 memberValues 中没有 "value" 键时 Object defaultValue = method.getDefaultValue(); // ← 从 AnnotationDefault 读取

这就是为什么 method.getDefaultValue() 能返回默认值!

3、生命周期阶段

3.1、生命周期

Java 注解的生命周期分为三个阶段,通过 @Retention(RetentionPolicy.XXX)指定。

如下所示:

保留策略(RetentionPolicy)生命周期能否被反射读取?典型用途
SOURCE仅存在于源码中,编译后丢弃@Override, @SuppressWarnings
CLASS存在于 .class文件中,JVM 加载时不加载到内存否(默认值)字节码分析工具(如 Lombok)
RUNTIME存在于.class 中,JVM 运行时可通过反射读取Spring, MyBatis, JUnit 等框架

关键点只有@Retention(RUNTIME)的注解才能在运行时通过getAnnotation()获取!

3.2、保留策略

1. RetentionPolicy.SOURCE

  • 作用:仅给编译器看,编译完就扔
  • 例子
@Override public String toString() { ... }
  • 编译器检查你是否真的重写了父类方法
  • 生成的 .class 文件中没有@Override

2. RetentionPolicy.CLASS(默认)

  • 作用:保留在字节码中,但 JVM 不加载到运行时

  • 用途:供字节码处理工具使用(如 ASM、Lombok)

  • 例子:Lombok 的 @Data

@Data // 编译时生成 getter/setter,运行时看不到这个注解 public class User { private String name; }

3. RetentionPolicy.RUNTIME

  • 作用:全程保留,运行时可通过反射获取

  • 这是框架最常用的策略!

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface LogExecutionTime {} // 使用 @LogExecutionTime public void process() { ... } // 反射读取 Method method = obj.getClass().getMethod("process"); if (method.isAnnotationPresent(LogExecutionTime.class)) { // 执行 AOP 逻辑 }

4、注意事项

1.推荐做法

  1. 优先使用RUNTIME:除非明确不需要运行时访问

  2. 提供默认值:减少使用者负担

String value() default "";
  1. @Documented:让注解出现在 API 文档中

  2. 命名清晰:如 @EnableCaching 比@CacheOn 更直观

2.避免陷阱

  • 不要滥用注解:过度使用会让代码难以理解

  • 注意性能:反射读取注解有开销(可缓存)

  • 线程安全:注解对象是不可变的,天然线程安全


参考文章:

1、一篇文章彻底明白重要概念——注解

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/16 11:41:38

Markdown高亮代码块:准确标注PyTorch语法

PyTorch-CUDA-v2.8 镜像实战指南&#xff1a;从环境搭建到高效开发 在深度学习项目中&#xff0c;最让人头疼的往往不是模型设计本身&#xff0c;而是“为什么我的代码在别人机器上跑不起来&#xff1f;”——这个问题背后&#xff0c;通常是 CUDA 版本、cuDNN 兼容性或 PyTorc…

作者头像 李华
网站建设 2026/5/2 11:17:34

SSH X11转发图形界面:可视化PyTorch训练过程

SSH X11转发图形界面&#xff1a;可视化PyTorch训练过程 在高校实验室或AI初创公司里&#xff0c;你是否遇到过这样的场景&#xff1f;一台搭载A100显卡的远程服务器正全力训练模型&#xff0c;而你的笔记本只能干等着——既看不到实时损失曲线&#xff0c;也无法查看特征图输出…

作者头像 李华
网站建设 2026/5/1 11:23:40

YOLOv11性能评测:对比YOLOv5/v8的目标检测精度与速度

YOLOv11性能评测&#xff1a;对比YOLOv5/v8的目标检测精度与速度 在智能摄像头遍布楼宇、工厂和道路的今天&#xff0c;一个核心问题始终困扰着算法工程师&#xff1a;如何在不牺牲实时性的前提下&#xff0c;让模型看得更准&#xff1f;尤其是在密集人群、远距离小目标等复杂场…

作者头像 李华
网站建设 2026/5/13 2:46:43

Transformer模型训练优化:借助PyTorch-CUDA-v2.8提速30%

Transformer模型训练优化&#xff1a;借助PyTorch-CUDA-v2.8提速30% 在大模型时代&#xff0c;一个再普通不过的调试场景可能是这样的&#xff1a;研究员提交了一次Transformer训练任务&#xff0c;显卡风扇轰鸣&#xff0c;监控脚本显示“预计剩余时间&#xff1a;72小时”。三…

作者头像 李华
网站建设 2026/5/3 10:38:29

接外包如何评估工时、给出报价?完整方法与实战技巧

诸神缄默不语-个人技术博文与视频目录 在软件开发外包市场中&#xff0c;如何准确评估项目工时、制定报价&#xff0c;是每个开发者或团队都会遇到的核心问题。报价太低容易赔钱、合同纠纷&#xff1b;报价太高又失去竞争力。今天我们从方法论和实操角度拆解这整个过程&#x…

作者头像 李华
网站建设 2026/5/6 9:54:38

HuggingFace Model Hub搜索技巧:发现优质预训练模型

HuggingFace Model Hub搜索技巧&#xff1a;发现优质预训练模型 在今天的AI研发中&#xff0c;一个常见的困境是&#xff1a;明明知道某个任务可以用BERT或T5来解决&#xff0c;却不知道从哪里找一个性能稳定、文档清晰、社区活跃的现成模型。手动复现论文中的结果&#xff1f;…

作者头像 李华