元注解详解
大翔站在白板前,指着屏幕上密密麻麻的注解代码说道:"注解不是魔法,它是元数据。而控制注解行为的,正是'元注解'——注解的注解。"
白歌推了推眼镜:"就像我们公司的规章制度,规章制度本身也需要被定义适用范围和有效期。"
小崔举手:"大翔哥,那 @Override 是怎么让编译器知道我在重写方法的?"
大翔笑了:"问得好。@Override 之所以能在编译期生效,是因为它的 @Retention 是 SOURCE。这就是元注解的力量——它定义了注解的'生存法则'。"
朱璐在旁边敲着键盘:"所以元注解就是注解的'配置中心'?"
一、定义表
| 元注解 | 作用 | JDK版本 | 取值示例 |
|---|---|---|---|
@Retention | 指定注解保留到哪个阶段 | JDK 5 | SOURCE, CLASS, RUNTIME |
@Target | 指定注解可以修饰哪些元素 | JDK 5 | TYPE, METHOD, FIELD, PARAMETER 等 |
@Documented | 标记注解是否出现在Javadoc中 | JDK 5 | 无参数,标记注解 |
@Inherited | 标记注解是否被自动继承 | JDK 5 | 无参数,标记注解 |
@Repeatable | 允许同一个元素重复标注同一注解 | JDK 8 | value = 容器注解.class |
二、Mermaid 类图:元注解体系结构
三、Mermaid 流程图:注解处理的两个阶段
四、@Retention 详解
@Retention 定义了注解的"生命周期",通过 RetentionPolicy 枚举指定:
| RetentionPolicy | 含义 | 典型场景 |
|---|---|---|
SOURCE | 仅保留在源码中,编译时丢弃 | @Override, @SuppressWarnings, @SafeVarargs |
CLASS | 保留到字节码中,但运行时不可通过反射获取 | Lombok 注解(如 @Data),JSR 308 类型注解 |
RUNTIME | 保留到运行时,反射可读取 | @Test, @Autowired, @Entity |
五、@Target 详解
@Target 限制注解可以修饰的程序元素,通过 ElementType 枚举数组指定:
| ElementType | 可修饰的元素 | 示例 |
|---|---|---|
TYPE | 类、接口、枚举、注解 | @Entity, @Service |
FIELD | 成员变量(含枚举常量) | @Autowired, @Column |
METHOD | 方法 | @Override, @Test |
PARAMETER | 方法参数 | @RequestParam, @NotNull |
CONSTRUCTOR | 构造器 | @Inject |
LOCAL_VARIABLE | 局部变量 | @SuppressWarnings 可用于局部变量 |
ANNOTATION_TYPE | 注解类型 | 元注解本身 |
PACKAGE | 包声明 | @NonNullApi |
TYPE_PARAMETER | 类型参数声明 | <@NonNull T> |
TYPE_USE | 任何类型使用处 | new @NonNull Object() |
六、@Documented 与 @Inherited
6.1 @Documented
当一个注解被 @Documented 修饰时,使用该注解的元素的 Javadoc 文档中会包含该注解信息。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CompanyDocument {
String name();
String date();
}
6.2 @Inherited
被 @Inherited 修饰的注解,其子类会自动继承父类的该注解。注意:@Inherited 仅对类继承有效,对接口继承无效。
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface InheritedAnnotation {
String value();
}
七、@Repeatable 详解(JDK 8 新增)
JDK 8 之前,同一个位置无法重复标注同一个注解。JDK 8 引入 @Repeatable 解决了这一痛点。
@Repeatable 需要一个"容器注解"来包裹多个重复注解:
// 容器注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Schedules {
Schedule[] value(); // 必须返回重复注解的数组
}
// 重复注解
@Repeatable(Schedules.class) // 指定容器注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Schedule {
String cron();
String desc() default "";
}
八、注解元素类型(Annotation Element)
注解中可以定义的元素类型必须是以下之一:
- 基本数据类型(int, float, boolean 等)
- String
- Class
- 枚举
- 其他注解类型
- 以上类型的数组
关键规则:
- 元素声明类似方法声明,不能带参数
- 可以用
default指定默认值 - 如果只有一个元素且名为
value,使用时可以省略value=
public @interface Employee {
int id();
String name() default "新员工";
double salary() default 8888.88;
String[] skills() default {};
}
九、完整代码示例
示例1:飞翔科技自定义@Validate校验注解
import java.lang.annotation.*;
import java.lang.reflect.Field;
/**
* 飞翔科技自定义校验注解 —— 配合元注解配置
*/
// 1. 标记注解:标识字段需要校验
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@interface NotNull {
String message() default "字段不能为 null";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@interface Min {
int value() default 0;
String message() default "值太小";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@interface Max {
int value() default 99999;
String message() default "值太大";
}
// 2. 组合校验注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@interface Validate {
boolean notNull() default false;
int min() default Integer.MIN_VALUE;
int max() default Integer.MAX_VALUE;
}
// 3. 飞翔科技员工类
class FeixiangEmployee {
@NotNull(message = "员工ID不能为空")
private Integer id;
@NotNull(message = "员工姓名不能为空")
private String name;
@Min(value = 3000, message = "薪资不能低于3000")
@Max(value = 50000, message = "薪资不能超过50000")
private double salary;
@Validate(notNull = true, min = 18, max = 65)
private int age;
public FeixiangEmployee(Integer id, String name, double salary, int age) {
this.id = id;
this.name = name;
this.salary = salary;
this.age = age;
}
@Override
public String toString() {
return "员工[id=" + id + ", name=" + name
+ ", salary=" + salary + ", age=" + age + "]";
}
}
// 4. 校验引擎
class Validator {
public static void validate(Object obj) throws Exception {
Class<?> clazz = obj.getClass();
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
Object value = field.get();
// 处理 @NotNull
NotNull notNull = field.getAnnotation(NotNull.class);
if (notNull != null && value == null) {
throw new RuntimeException(notNull.message());
}
// 处理 @Min
Min min = field.getAnnotation(Min.class);
if (min != null && value instanceof Number) {
double numVal = ((Number) value).doubleValue();
if (numVal < min.value()) {
throw new RuntimeException(min.message() + ",当前值: " + numVal);
}
}
// 处理 @Max
Max max = field.getAnnotation(Max.class);
if (max != null && value instanceof Number) {
double numVal = ((Number) value).doubleValue();
if (numVal > max.value()) {
throw new RuntimeException(max.message() + ",当前值: " + numVal);
}
}
}
System.out.println("✓ " + obj + " 校验通过!");
}
}
// 5. 测试主类
public class FeixiangAnnotationDemo {
public static void main(String[] args) {
System.out.println("========== 飞翔科技注解校验系统 ==========\n");
// 测试1:合法员工
FeixiangEmployee emp1 = new FeixiangEmployee(1001, "大翔", 18888.88, 28);
try {
Validator.validate(emp1);
} catch (Exception e) {
System.out.println("✗ 校验失败: " + e.getMessage());
}
// 测试2:薪资过低
FeixiangEmployee emp2 = new FeixiangEmployee(1002, "小崔", 2500.0, 22);
try {
Validator.validate(emp2);
} catch (Exception e) {
System.out.println("✗ 校验失败: " + e.getMessage());
}
// 测试3:姓名为空
FeixiangEmployee emp3 = new FeixiangEmployee(1003, null, 8888.88, 30);
try {
Validator.validate(emp3);
} catch (Exception e) {
System.out.println("✗ 校验失败: " + e.getMessage());
}
// 测试4:年龄超限
FeixiangEmployee emp4 = new FeixiangEmployee(1004, "朱璐", 12000.0, 70);
try {
Validator.validate(emp4);
} catch (Exception e) {
System.out.println("✗ 校验失败: " + e.getMessage());
}
System.out.println("\n========== End ==========");
}
}
运行输出:
========== 飞翔科技注解校验系统 ==========
✓ 员工[id=1001, name=大翔, salary=18888.88, age=28] 校验通过!
✗ 校验失败: 薪资不能低于3000,当前值: 2500.0
✗ 校验失败: 员工姓名不能为空
✗ 校验失败: 年龄不能超过65,当前值: 70.0
========== End ==========
示例2:飞翔科技@Schedule定时任务注解(@Repeatable)
import java.lang.annotation.*;
import java.lang.reflect.Method;
import java.util.*;
/**
* 飞翔科技定时任务调度系统 —— 演示 @Repeatable 用法
*/
// 1. 容器注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface TaskSchedules {
TaskSchedule[] value();
}
// 2. 可重复的定时任务注解
@Repeatable(TaskSchedules.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface TaskSchedule {
String cron(); // cron 表达式
String desc() default ""; // 任务描述
int priority() default 5; // 优先级 1-10
}
// 3. 任务处理器
class TaskProcessor {
@TaskSchedule(cron = "0 0 9 * * MON-FRI", desc = "早晨站会提醒", priority = 8)
@TaskSchedule(cron = "0 30 12 * * MON-FRI", desc = "午休提醒", priority = 3)
public void dailyMeetingReminder() {
System.out.println(" [执行] 日常会议提醒任务");
}
@TaskSchedule(cron = "0 0 0 1 * *", desc = "月初财务报表生成", priority = 10)
@TaskSchedule(cron = "0 0 0 15 * *", desc = "月中财务复核", priority = 9)
@TaskSchedule(cron = "0 0 0 L * *", desc = "月末财务结算", priority = 10)
public void financialReport() {
System.out.println(" [执行] 财务报告生成任务");
}
@TaskSchedule(cron = "0 0 2 * * SUN", desc = "数据库备份", priority = 7)
public void databaseBackup() {
System.out.println(" [执行] 数据库备份任务");
}
@TaskSchedule(cron = "0 */5 * * * *", desc = "系统健康检查", priority = 6)
@TaskSchedule(cron = "0 */1 * * * *", desc = "用户活跃度统计", priority = 4)
public void systemMonitor() {
System.out.println(" [执行] 系统监控任务");
}
}
// 4. 调度器
class Scheduler {
public static void scanAndPrintTasks(Object obj) {
Class<?> clazz = obj.getClass();
System.out.println("扫描类: " + clazz.getSimpleName());
System.out.println("=".repeat(60));
for (Method method : clazz.getDeclaredMethods()) {
// 使用 getAnnotationsByType 读取 @Repeatable 注解(JDK 8 新增方法)
TaskSchedule[] schedules = method.getAnnotationsByType(TaskSchedule.class);
if (schedules.length > 0) {
System.out.println("\n方法: " + method.getName());
System.out.println("-".repeat(40));
for (TaskSchedule schedule : schedules) {
System.out.printf(" ├─ Cron: %-25s | 描述: %-15s | 优先级: %d\n",
schedule.cron(), schedule.desc(), schedule.priority());
}
}
}
// 也展示 getAnnotation 获取容器注解的方式
System.out.println("\n" + "=".repeat(60));
System.out.println("(以下展示通过容器注解 TaskSchedules 获取)\n");
for (Method method : clazz.getDeclaredMethods()) {
TaskSchedules container = method.getAnnotation(TaskSchedules.class);
if (container != null) {
System.out.println("方法 " + method.getName() + " 有 "
+ container.value().length + " 个定时任务");
}
}
}
}
// 5. 主程序
public class FeixiangScheduleDemo {
public static void main(String[] args) {
System.out.println("========== 飞翔科技定时任务调度系统 ==========\n");
TaskProcessor processor = new TaskProcessor();
Scheduler.scanAndPrintTasks(processor);
System.out.println("\n" + "=".repeat(60));
System.out.println("飞翔科技的总监 Frank 说道:");
System.out.println("\"有了 @Repeatable,一个方法可以挂多个定时任务,代码清爽多了!\"");
System.out.println("\n========== End ==========");
}
}
运行输出:
========== 飞翔科技定时任务调度系统 ==========
扫描类: TaskProcessor
============================================================
方法: dailyMeetingReminder
----------------------------------------
├─ Cron: 0 0 9 * * MON-FRI | 描述: 早晨站会提醒 | 优先级: 8
├─ Cron: 0 30 12 * * MON-FRI | 描述: 午休提醒 | 优先级: 3
方法: financialReport
----------------------------------------
├─ Cron: 0 0 0 1 * * | 描述: 月初财务报表生成 | 优先级: 10
├─ Cron: 0 0 0 15 * * | 描述: 月中财务复核 | 优先级: 9
├─ Cron: 0 0 0 L * * | 描述: 月末财务结算 | 优先级: 10
方法: databaseBackup
----------------------------------------
├─ Cron: 0 0 2 * * SUN | 描述: 数据库备份 | 优先级: 7
方法: systemMonitor
----------------------------------------
├─ Cron: 0 */5 * * * * | 描述: 系统健康检查 | 优先级: 6
├─ Cron: 0 */1 * * * * | 描述: 用户活跃度统计 | 优先级: 4
============================================================
(以下展示通过容器注解 TaskSchedules 获取)
方法 dailyMeetingReminder 有 2 个定时任务
方法 financialReport 有 3 个定时任务
方法 systemMonitor 有 2 个定时任务
============================================================
飞翔科技的总监 Frank 说道:
"有了 @Repeatable,一个方法可以挂多个定时任务,代码清爽多了!"
========== End ==========
十、易错场景
易错1:@Retention 设为 CLASS 导致反射获取不到
// 错误示例
@Retention(RetentionPolicy.CLASS) // ← 编译后保留但运行时不可反射访问!
@Target(ElementType.METHOD)
@interface MyLog {}
public class ErrorDemo1 {
@MyLog
public void doWork() {}
public static void main(String[] args) throws Exception {
Method m = ErrorDemo1.class.getMethod("doWork");
MyLog anno = m.getAnnotation(MyLog.class);
System.out.println(anno); // 输出: null ← 注解在运行时消失了!
}
}
正确做法:如果需要在运行时通过反射读取注解,必须使用 RetentionPolicy.RUNTIME。
@Retention(RetentionPolicy.RUNTIME) // ← 正确:运行时可见
@Target(ElementType.METHOD)
@interface MyLog {}
易错2:@Inherited 不会通过接口继承
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface FeixiangAnnotation {}
@FeixiangAnnotation
interface BaseService {}
class UserService implements BaseService {} // UserService 不会继承 @FeixiangAnnotation
// 验证
public class ErrorDemo2 {
public static void main(String[] args) {
// 接口继承:输出 null
System.out.println(UserService.class.getAnnotation(FeixiangAnnotation.class)); // null!
// @Inherited 只对类继承生效,对接口继承无效
}
}
易错3:@Repeatable 容器注解 value() 返回类型不匹配
// 错误示例
@interface BugReports {
String[] value(); // ← 错误!必须返回 BugReport[],不能是 String[]
}
@Repeatable(BugReports.class)
@interface BugReport {
String value();
}
// 编译错误:containing annotation type must declare value() element with return type BugReport[]
正确做法:容器注解的 value() 方法必须返回被包含注解类型的数组。
易错4:注解元素不能是包装类型和复杂对象
public @interface WrongAnnotation {
Integer count(); // ← 编译错误!不能用包装类型,只能用 int
List<String> tags(); // ← 编译错误!不能用集合类型
Date updateDate(); // ← 编译错误!只能用 String、Class、枚举、注解等基础类型
}
// 正确做法
public @interface CorrectAnnotation {
int count(); // ✓ 基本类型
String[] tags(); // ✓ String 数组
Class<?> entityClass(); // ✓ Class 类型
ElementType type(); // ✓ 枚举类型
}
十一、面试考点
考点1:@Retention 的三种策略分别在什么场景下使用?
答:
- SOURCE:仅编译期需要,如
@Override给编译器检查方法重写是否正确;@SuppressWarnings抑制编译器警告。这类注解编译后就不需要了。 - CLASS:需要保留在字节码中但运行时无需通过反射访问。典型如 Lombok 的
@Data、@Getter——它们需要在编译期修改字节码,但运行期不需要感知。这也是默认策略。 - RUNTIME:运行时需要通过反射读取的注解,如 Spring 的
@Autowired、JUnit 的@Test、Hibernate 的@Entity。几乎所有框架注解都用 RUNTIME。
考点2:@Repeatable 的容器注解必须满足什么条件?
答:
- 容器注解的
value()方法必须返回被重复注解类型的数组。 - 容器注解的
@Retention和@Target必须至少与被重复注解一样宽泛。 - 使用
getAnnotationsByType(Xxx.class)(JDK 8 新增)来读取重复注解,传统getAnnotation()只能读到容器注解。
考点3:@Inherited 的继承机制有什么限制?
答:
- 只对类的继承生效——子类继承父类的 @Inherited 注解。
- 对接口继承无效——子接口不会继承父接口的 @Inherited 注解。
- 对接口实现无效——实现类不会从接口继承 @Inherited 注解。
- 对方法重写无效——子类重写的方法不会继承父类方法上的 @Inherited 注解(@Inherited 只能标注类级别)。
考点4:ElementType.TYPE_USE 和 TYPE_PARAMETER 是什么?JDK 8 新增的有什么意义?
答: JDK 8 新增了两个 ElementType 值,用于支持 JSR 308(类型注解):
- TYPE_PARAMETER:可用在类型参数声明处,如
class Box<@NonNull T>。 - TYPE_USE:可用在任何使用类型的地方,如
new @NonNull Object()、(@NonNull String) obj。
这为 Checker Framework 等静态分析工具提供了基础,使得类型检查可以在编译期进行(如空安全检查 @Nullable、@NonNull)。
十二、JDK 8 元注解速查表
| 元注解 | 属性 | JDK 8 新增 | 常用场景 |
|---|---|---|---|
@Retention | RetentionPolicy value() | - | 控制注解生命周期 |
@Target | ElementType[] value() | TYPE_PARAMETER, TYPE_USE | 限制注解适用范围 |
@Documented | - | - | Javadoc 文档中可见 |
@Inherited | - | - | 子类自动继承注解 |
@Repeatable | Class<? extends Annotation> value() | ✓ | 同一位置重复标注 |
AnnotatedElement.getAnnotationsByType() | - | ✓ (新增方法) | 读取 @Repeatable 注解 |
白歌合上笔记本总结道:"元注解就像是给注解编程。@Retention 决定'活多久',@Target 决定'用在哪',@Repeatable 允许'反复说'——掌握了这五个元注解,就掌握了注解体系的底层逻辑。"
大翔点点头:"没错。Spring Boot 的自动配置、JUnit 的测试框架、Lombok 的代码生成,底层都是元注解在发挥作用。它们是 Java 元编程的基石。"
黄俪(HR)路过,插了一句:"你们能不能用元注解给我写个自动排班系统?"
众人笑:"这个需求我们记下了!"