注解(元注解/运行时注解)
很多同学用 @Override、@Deprecated 的时候,觉得注解就是一个"标记",没什么技术含量。但真正面试追问下去——"注解是怎么被处理的?运行时注解怎么通过反射拿到?元注解 @Retention 三个值分别是什么意思?"——能答全的人不多。
我自己之前也只停留在"会用"层面,直到有一次项目里需要写一个自动参数校验的框架,才认真去看了注解处理器和反射的配合。这篇把注解从表层到里层全部过一遍。
一、什么是注解
注解(Annotation)是 Java 5 引入的语法糖,本质是一种元数据形式,附加在类、方法、字段、参数等程序元素上,为编译器、构建工具或运行时提供额外信息。
【直观类比】
注解就像贴在家电上的标签。"CCC 认证"标签告诉消费者这个产品符合标准,"能效等级 A"告诉你它省不省电。这些标签不影响家电本身的运行逻辑,但可以被质检部门、消费者、回收机构识别和处理。注解也一样——它本身不改变代码行为,但可以被编译器、工具链或框架读取并采取行动。
二、元注解详解
元注解是"注解的注解",用来声明自定义注解的行为。Java 内置了 4 个元注解,全部定义在 java.lang.annotation 包里。
2.1 @Target — 注解能用在哪
@Target(ElementType.METHOD) // 只能用在方法上
@Target({ElementType.METHOD, ElementType.FIELD}) // 可以用在方法和字段上
⚠️
如果 @Target 不声明任何值,那注解可以用在任何程序元素上。但这种情况非常罕见,通常应该明确限定注解的使用范围。
2.2 @Retention — 注解保留到哪个阶段
这是最容易被问到的元注解,决定了注解在哪个阶段可见:
@Retention(RetentionPolicy.SOURCE) // 只在源码阶段,编译后丢弃
@Retention(RetentionPolicy.CLASS) // 保留到类文件,但运行时不可见(JDK 默认)
@Retention(RetentionPolicy.RUNTIME) // 一直保留,运行时可通过反射读取
💡
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 提供了一些常用的内置注解:
@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) {
// ...
}
}
注解的属性定义看起来像接口方法,但实际上是注解的参数。属性类型只能是:
- 基本类型(
int、long、boolean 等)
String
Class 或 Class<?>
- 枚举类型
- 注解类型
- 以上类型的数组
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) 的注解才能被反射读取。SOURCE 和 CLASS 级别的注解在运行时根本不存在。
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 的原理就是编译时注解处理器——编译器在处理过程中动态插入了字节码。