跳到主要内容
约 8 分钟3 张卡

Java 注解

Java 注解为类、方法和字段等程序元素附加结构化元数据。元数据就是描述这些元素的额外信息,例如方法要求的角色名称。编译器、字节码工具或框架读取这些信息后,才会执行对应行为。

本文从注解的定义入手,依次说明使用位置、保留策略和处理阶段。最后介绍自定义注解的设计,以及重复注解的读取规则。

1. 面试核心​

先记住这几条,再读正文
  • 元数据与行为。 注解保存程序元素的元数据,编译器、字节码工具或框架读取后执行对应行为。卡 · 注解定义
  • 保留策略。 SOURCE 注解只保留在源码中,CLASS 注解写入类文件,RUNTIME 注解还支持运行时反射读取。卡 · @Retention
  • 默认策略与使用位置。 @Retention 默认取 CLASS,@Target 约束使用位置。
  • 继承查找。 @Inherited 让类注解沿超类链被查找到,接口和成员注解遵循各自的查找规则。
  • 重复注解。 getAnnotationsByType 可展开重复注解的容器。卡 · 重复注解的运行时读取

2. 注解定义​

自定义注解需要先声明类型,再定义它能保存哪些配置。Java 使用 @interface 定义注解类型:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RequiresRole {
String value();
}

注解中的 value() 是注解元素,用来声明配置项的名称和类型。这里的配置项类型为 String,使用注解时为它提供角色名称:

@RequiresRole("admin")
public void deleteUser(long userId) {
// ...
}

上例把 "admin" 保存到 value 元素中。元素名为 value,且其他元素均有默认值时,可以省略元素名。

元素还可以声明默认值,让使用方只填写需要调整的配置:

public @interface Retry {
int maxAttempts() default 3;
long delayMillis() default 100;
}

省略 @Retry 的配置时,maxAttempts 为 3,delayMillis 为 100。

注解元素支持基本类型、String、Class、枚举和注解,也支持这些类型的一维数组。元素值和默认值都必须是非 null 值,因此可选配置需要用明确的默认值表达。

3. 元注解​

元注解用于描述注解类型本身。通过它们,编译器和读取工具能知道注解可以放在哪里、保留多久,以及如何处理重复声明。

3.1 @Target​

@Target 限制注解的使用位置,让编译器在发现误用时直接报错。例如:

@Target({ElementType.TYPE, ElementType.METHOD})

它表示注解可以放在类型和方法上。常见目标还包括字段、构造方法、参数、局部变量、类型参数和类型使用位置。

未声明 @Target 时,可用于类型参数之外的声明位置。类型使用位置需要显式声明 TYPE_USE。明确目标可以让编译器尽早阻止误用。

3.2 @Retention​

@Retention 决定注解保留到哪个阶段。需要在运行时读取的注解,可以这样声明:

@Retention(RetentionPolicy.RUNTIME)

选择时需要区分以下三种保留策略:

策略源码中存在class 文件中存在运行时反射可见常见用途
SOURCE是否否编译检查、源码处理
CLASS是是否字节码分析和转换
RUNTIME是是是运行时框架读取

没有声明 @Retention 时,默认策略是 CLASS。

保留策略取决于谁在什么时候读取注解。只供当前编译使用时,SOURCE 已经足够。如果后续工具要分析类文件,就需要至少保留到 CLASS。框架通过反射读取时,则要使用 RUNTIME。

3.3 @Documented​

带有 @Documented 的注解会被 Javadoc 等文档工具纳入被标注元素的公开文档。它只影响文档呈现,不改变运行时行为。

3.4 @Inherited​

@Inherited 用于让子类查询到父类上的类注解。注解同时保留到运行时时,Class.getAnnotation 会沿超类链查找。

使用时需要区分以下查找范围:

  • 只作用于类上的注解。
  • 实现类上的查询会跳过接口注解,接口需要由框架另行搜索。
  • 父类方法或字段上的注解仍属于原成员,子类声明的对应成员需要单独处理。
  • 使用 getDeclaredAnnotation 时只检查当前类,不沿超类查找。

框架合并接口、方法和组合注解时,需要实现自己的搜索规则。

3.5 @Repeatable​

@Repeatable 允许在同一个程序元素上多次使用同一种注解。编译器用容器注解保存这些实例,因此需要同时定义两种注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(Roles.class)
public @interface Role {
String value();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Roles {
Role[] value();
}

@Role("reader")
@Role("editor")
final class DocumentService {}

读取时使用 getAnnotationsByType(Role.class),可以同时兼容单个注解和容器形式。

4. 注解的处理阶段​

同一个标记可以供不同工具使用,具体行为取决于读取它的工具。按读取时间,可以区分编译阶段、类文件处理阶段和运行阶段。

4.1 编译阶段​

编译阶段可以根据注解检查源码,或提前生成运行时需要的代码。比如,@Override 让编译器检查方法是否正确重写,@SuppressWarnings 用于抑制指定警告。自定义处理器还可以读取程序元素,报告编译错误,或生成新的源码与资源文件。

MapStruct 用注解处理器生成映射代码。Lombok 也参与编译过程,但它修改的是编译器内部的语法树,也就是编译器组织源码的结构。

生成新文件与修改语法树的区别,见 Java 注解处理器。

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface GenerateMapper {}

如果处理器只需要在编译期读取 @GenerateMapper,使用 SOURCE 已经足够。生成后的类会作为普通代码继续编译,运行时不必保留这个标记。

4.2 class 文件处理阶段​

CLASS 注解写入类文件,供后续字节码工具分析和修改代码。这个阶段适合静态分析、插桩和字节码变换。插桩指向已有字节码插入额外逻辑,例如记录方法耗时。

类文件分别保存运行时可见和不可见的注解属性。字节码工具需要解析对应属性,才能使用其中的元数据。反射 API 只返回运行时可见的注解,所以读取 CLASS 注解需要字节码工具支持。

4.3 运行阶段​

运行时框架可以通过反射读取 RUNTIME 注解:

Method method = UserService.class
.getDeclaredMethod("deleteUser", long.class);

RequiresRole rule = method.getAnnotation(RequiresRole.class);

if (rule != null) {
authorization.check(rule.value());
}

注解只提供角色名称,权限校验由框架调用 authorization.check 完成。如果没有任何代码读取注解,@RequiresRole 不会自动阻止方法执行。

Spring 的 @Transactional 也由框架读取配置,在代理调用中控制事务。具体执行过程见 Spring 事务。

5. 自定义注解设计​

自定义注解要同时设计配置与读取逻辑。设计前需要明确以下五个问题:

  1. 谁读取这个注解:编译器插件、构建工具还是运行时框架?
  2. 标记放在哪些元素上?
  3. 注解元素表达的是稳定配置,还是应该由普通代码完成的业务逻辑?
  4. 重复使用、继承和组合时采用什么规则?
  5. 配置无效时在编译期还是运行时报告错误?

注解适合声明简短、静态且与程序元素紧密相关的元数据。复杂条件、动态查询和控制流更适合普通对象与方法。

5.1 元数据与执行逻辑​

注解适合保存配置,读取方负责完成操作。例如,重试注解可以声明最大尝试次数和间隔:

@Retry(maxAttempts = 3, delayMillis = 200)
void callPayment() {}

拦截器或显式调用器读取配置后,捕获允许重试的异常。随后它等待指定间隔,再次调用原方法。

要让重试安全,还需要明确以下三项业务规则:

  • 操作是否幂等,也就是重复调用是否会产生额外业务结果。
  • 哪些异常允许重试,例如临时连接失败和参数错误应区别处理。
  • 总超时是多少,等待和多次调用都要计入这个限制。

5.2 注解默认值​

默认值会影响所有省略该元素的使用点。即使业务源码未改,读取方使用新的默认值后,行为也会改变。因此,公开注解应写清默认值的含义,危险操作则要求调用方显式配置。

6. 重复注解的运行时读取​

重复使用 @Role 时,编译器用 @Roles 保存多个实例。两个注解都需使用 RUNTIME,才能在运行时读取。

Role[] roles = DocumentService.class.getAnnotationsByType(Role.class);
System.out.println(roles.length); // 2

getAnnotation(Role.class) 不负责展开容器。单个或重复声明统一使用 getAnnotationsByType。

7. 常见问题​

7.1 为什么反射获取不到自定义注解?​

先检查注解是否声明为 RetentionPolicy.RUNTIME,再检查读取的元素是否正确。类注解、方法注解和字段注解分别属于不同元素;父类、接口和重写方法也不会按照同一规则自动继承。

7.2 注解可以继承另一个注解吗?​

7.3 修改注解值后,程序什么时候使用新配置?​

7.4 @Inherited 为什么对接口无效?​

8 · 面试题卡片​

本页题目与参考回答

8.1 SOURCE、CLASS 和 RUNTIME 有什么区别​

出现公司:飞书、汇众网络科技

考察重点

  • 说明三种保留策略分别在哪个阶段可见。
  • 根据编译期处理、字节码处理和反射读取的需要选择策略。
  • 解释省略 @Retention 时会采用什么策略。

相关内容:@Retention、注解的处理阶段。

参考回答

SOURCE 只保留在源码中,CLASS 保留到类文件,RUNTIME 还支持运行时反射读取。

SOURCE 适合编译检查和源码处理。CLASS 供字节码工具读取,也是省略 @Retention 时的默认策略。需要框架通过反射扫描注解时,应使用 RUNTIME。

选择时还要区分当前编译和后续编译。当前编译中生成代码可用 SOURCE。如果后续编译还要读取依赖类中的注解,就需要把它保留到 CLASS 或 RUNTIME。

8.2 自定义注解怎样产生实际行为​

出现公司:商汤科技、携程、恒生电子

考察重点

  • 区分注解保存的配置与读取方执行的逻辑。
  • 说明编译期处理器和运行时反射分别怎样使用注解。
  • 根据读取方的需求设计 @Target、@Retention 和注解元素。

相关内容:注解定义、注解的处理阶段、自定义注解设计。

参考回答

自定义注解由读取它的编译器工具或框架解释配置,再执行对应行为。

定义时用 @Target 限制使用位置,用 @Retention 确定保留阶段,再用注解元素保存配置。编译期处理器可以据此校验程序或生成代码。运行时框架则通过反射读取 RUNTIME 注解,再执行权限、事务或路由逻辑。

8.3 重复注解为什么用 getAnnotationsByType 读取?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:解释容器怎样保存重复注解,并根据保留策略选择读取 API。

相关内容:重复注解的运行时读取。

参考回答

getAnnotationsByType 会展开容器中的重复注解,因此可以统一读取单个和多个声明。

例中两个 @Role 被放进 @Roles 容器。Role 与 Roles 都保留到运行时,所以调用结果包含两个角色。getAnnotation(Role.class) 只查目标注解本身,不会自动展开容器。

9. 参考资料​