注解(元注解/运行时注解)

很多同学用 @Override@Deprecated 的时候,觉得注解就是一个"标记",没什么技术含量。但真正面试追问下去——"注解是怎么被处理的?运行时注解怎么通过反射拿到?元注解 @Retention 三个值分别是什么意思?"——能答全的人不多。

我自己之前也只停留在"会用"层面,直到有一次项目里需要写一个自动参数校验的框架,才认真去看了注解处理器和反射的配合。这篇把注解从表层到里层全部过一遍。

一、什么是注解

注解(Annotation)是 Java 5 引入的语法糖,本质是一种元数据形式,附加在类、方法、字段、参数等程序元素上,为编译器、构建工具或运行时提供额外信息。

【直观类比】

注解就像贴在家电上的标签。"CCC 认证"标签告诉消费者这个产品符合标准,"能效等级 A"告诉你它省不省电。这些标签不影响家电本身的运行逻辑,但可以被质检部门、消费者、回收机构识别和处理。注解也一样——它本身不改变代码行为,但可以被编译器、工具链或框架读取并采取行动。

二、元注解详解

元注解是"注解的注解",用来声明自定义注解的行为。Java 内置了 4 个元注解,全部定义在 java.lang.annotation 包里。

2.1 @Target — 注解能用在哪

@Target(ElementType.METHOD)           // 只能用在方法上
@Target({ElementType.METHOD, ElementType.FIELD})  // 可以用在方法和字段上
ElementType 值含义
TYPE类、接口、枚举
FIELD字段(包括枚举常量)
METHOD方法
PARAMETER方法参数
CONSTRUCTOR构造方法
LOCAL_VARIABLE局部变量
ANNOTATION_TYPE注解类型
PACKAGE
⚠️

如果 @Target 不声明任何值,那注解可以用在任何程序元素上。但这种情况非常罕见,通常应该明确限定注解的使用范围。

2.2 @Retention — 注解保留到哪个阶段

这是最容易被问到的元注解,决定了注解在哪个阶段可见:

@Retention(RetentionPolicy.SOURCE)   // 只在源码阶段,编译后丢弃
@Retention(RetentionPolicy.CLASS)     // 保留到类文件,但运行时不可见(JDK 默认)
@Retention(RetentionPolicy.RUNTIME)  // 一直保留,运行时可通过反射读取
保留阶段RetentionPolicy典型用途能否反射读取
源码SOURCE@Override@SuppressWarnings
类文件CLASS编译期注解处理器,字节码增强(如 Lombok)
运行时RUNTIME运行时框架(Spring、Jackson)
💡

RUNTIME 是面试常考点。如果自定义注解需要通过反射读取,必须设置 @Retention(RetentionPolicy.RUNTIME)。否则编译后注解就丢了,反射根本拿不到。

2.3 @Documented — 是否包含在 Javadoc 中

@Documented
public @interface MyAnnotation {
    String value();
}

加上 @Documented 后,使用该注解的元素的 Javadoc 里会包含这个注解的信息。不加的话,注解不会出现在生成的文档中。

2.4 @Inherited — 是否可继承

@Inherited
public @interface MyAnnotation {}

如果父类标记了 @Inherited 注解,子类会自动继承该注解。这个特性在框架设计中偶尔会用到(比如自定义权限注解),但大多数业务代码用不到。

三、内置注解

Java 提供了一些常用的内置注解:

注解@TargetRetention含义
@OverrideMETHODSOURCE编译检查该方法是否是覆盖父类方法
@DeprecatedallRUNTIME标记已废弃,编译器警告,Javadoc 包含
@SuppressWarningsTYPE, FIELD, METHOD, PARAMETER, ...CLASS让编译器忽略指定的警告
@FunctionalInterfaceTYPERUNTIME标记函数式接口,编译器检查是否符合
@SafeVarargsMETHOD, CONSTRUCTORRUNTIME压制可变参数泛型的堆污染警告
@FunctionalInterface
public interface Runnable {
    void run();
}

@Override
public void run() {
    // 编译器检查:这个方法确实覆盖了父类/接口的 run()
}

四、自定义注解

4.1 基本语法

// 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
    String value() default "";    // 注解属性,默认值为空字符串
    LogLevel level() default LogLevel.INFO;  // 带默认值,可选
}

// 使用
public class UserService {
    @MyLog(value = "用户登录", level = LogLevel.INFO)
    public void login(String username, String password) {
        // ...
    }
}

注解的属性定义看起来像接口方法,但实际上是注解的参数。属性类型只能是:

  • 基本类型(intlongboolean 等)
  • String
  • ClassClass<?>
  • 枚举类型
  • 注解类型
  • 以上类型的数组

4.2 运行时通过反射读取

public class AnnotationProcessor {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = UserService.class;
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.isAnnotationPresent(MyLog.class)) {
                MyLog log = method.getAnnotation(MyLog.class);
                System.out.println("方法: " + method.getName());
                System.out.println("日志: " + log.value());
                System.out.println("级别: " + log.level());
            }
        }
    }
}

关键方法:

  • isAnnotationPresent(Class):判断是否存在指定注解
  • getAnnotation(Class):获取注解实例
  • getAnnotationsByType(Class):获取所有该类型注解(包含继承的)
⚠️

只有 @Retention(RetentionPolicy.RUNTIME) 的注解才能被反射读取。SOURCECLASS 级别的注解在运行时根本不存在。

4.3 注解处理器(编译时)

如果要在编译期处理注解,需要实现 AnnotationProcessor

public class MyAnnotationProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(MyAnnotation.class)) {
            // 在编译阶段处理,比如生成代码
            System.out.println("处理注解: " + element.getSimpleName());
        }
        return true;
    }
}

Lombok 就是这个原理——编译时注解处理器在 class 文件里插入了 getter/setter 的字节码,IDE 看到的源码没变,但编译出来的 .class 包含了完整方法。

五、生产实战场景

5.1 参数校验

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {
    String message() default "参数不能为空";
}

public Object invoke(Method method, Object[] args) {
    Parameter[] params = method.getParameters();
    for (int i = 0; i < params.length; i++) {
        if (params[i].isAnnotationPresent(NotNull.class)) {
            if (args[i] == null) {
                NotNull notNull = params[i].getAnnotation(NotNull.class);
                throw new IllegalArgumentException(notNull.message());
            }
        }
    }
    return method.invoke(this, args);
}

5.2 日志切面

配合 Spring AOP,用注解标记需要记录日志的方法:

@Aspect
@Component
public class LogAspect {
    @Around("@annotation(myLog)")
    public Object around(ProceedingJoinPoint pjp, MyLog myLog) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();
        log.info("{} - 耗时: {}ms", myLog.value(), System.currentTimeMillis() - start);
        return result;
    }
}

六、常见误区

❌ 注解可以带参数行为吗

注解本身只是元数据,不包含任何执行逻辑。注解需要配合注解处理器(编译器插件)、反射代码或 AOP 框架才能发挥作用。

❌ 注解可以继承吗

注解默认不可继承。只有标记了 @Inherited 的注解才会在子类继承父类的标记。

【学习小结】

注解的核心是元数据 + 处理机制。元注解决定注解本身的行为:@Target 限定使用位置,@Retention 决定保留阶段,@Documented@Inherited 控制文档和继承。运行时注解靠反射读取,编译时注解靠处理器处理。Lombok 的原理就是编译时注解处理器——编译器在处理过程中动态插入了字节码。