Java 注解(Annotation)核心面试题+自定义注解全解析
一、Java 注解的原理是什么?
核心回答
Java 注解(Annotation)是 JDK 5 引入的元数据(Metadata)机制 ,本质是继承了 java.lang.annotation.Annotation 接口的特殊接口 ,用于给代码(类、方法、变量等)添加额外的标记信息,这些信息可以在编译期、类加载期、运行期被读取和处理,实现无侵入式的代码增强。
原理拆解
-
本质是接口 :我们自定义的
@MyAnnotation,编译后会生成一个MyAnnotation.class文件,它是一个继承了Annotation接口的接口 ,不是普通类。java// 自定义注解 public @interface MyAnnotation {} // 编译后等价于(JVM视角) public interface MyAnnotation extends Annotation {} -
元数据的作用 :注解本身不会直接影响代码逻辑,只是给代码打标签,真正的逻辑由**注解处理器(Annotation Processor)**实现。
-
生命周期 :注解的作用由
@Retention元注解控制,决定了注解信息保留到哪个阶段(源码、字节码、运行期)。 -
核心价值 :实现解耦 ,把配置信息和业务代码分离,替代传统的 XML 配置,是 Spring、MyBatis 等框架的核心基础(如
@Controller、@Service、@Autowired)。
二、注解解析的底层实现原理
核心回答
注解的解析本质是通过 Java 反射机制,读取类、方法、字段上的注解信息,并执行对应的逻辑 ,底层依赖 JDK 的 java.lang.reflect 包和 AnnotatedElement 接口。
底层实现拆解
1. 核心接口:AnnotatedElement
所有可以被注解标记的元素(类 Class、方法 Method、字段 Field、构造器 Constructor 等),都实现了 AnnotatedElement 接口,该接口提供了获取注解的核心方法:
getAnnotation(Class<T> annotationClass):获取指定类型的注解getAnnotations():获取所有注解(包含继承的)getDeclaredAnnotation(Class<T> annotationClass):获取当前元素直接声明的注解(不包含继承)isAnnotationPresent(Class<? extends Annotation> annotationClass):判断是否存在指定注解
2. 解析的完整流程
- 获取目标元素的反射对象 :比如通过
Class.forName()获取类对象,getMethod()获取方法对象。 - 判断注解是否存在 :通过
isAnnotationPresent()检查元素上是否标记了目标注解。 - 获取注解实例 :通过
getAnnotation()获取注解对象,本质是 JDK 动态生成的代理对象 ($ProxyN)。 - 读取注解属性 :通过代理对象调用注解的方法(如
value()、name()),获取注解中配置的参数。 - 执行自定义逻辑 :根据注解的属性值,执行对应的业务逻辑(如 Spring 扫描
@Controller注册 Bean、AOP 拦截@RequestMapping路由请求)。
3. 关键原理:动态代理生成注解实例
当我们调用 getAnnotation(MyAnnotation.class) 时,JVM 并不会直接返回我们定义的注解类,而是通过动态代理 ,在运行期生成一个实现了 MyAnnotation 接口的代理对象,这个代理对象持有注解的所有属性值,我们调用注解的方法时,本质是调用代理对象的方法,返回缓存的属性值。
4. 代码示例:自定义注解解析
java
// 1. 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
String value() default "";
}
// 2. 业务类,使用注解
public class UserService {
@MyLog("查询用户")
public void getUser() {
System.out.println("执行getUser方法");
}
}
// 3. 注解解析器(底层实现)
public class AnnotationParser {
public static void parse(Object obj) throws Exception {
Class<?> clazz = obj.getClass();
// 获取所有方法
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
// 判断方法上是否有MyLog注解
if (method.isAnnotationPresent(MyLog.class)) {
// 获取注解实例(动态代理对象)
MyLog annotation = method.getAnnotation(MyLog.class);
// 读取注解属性
String value = annotation.value();
System.out.println("方法:" + method.getName() + ",注解值:" + value);
// 执行自定义逻辑(如日志记录、权限校验)
System.out.println("记录日志:" + value);
}
}
}
public static void main(String[] args) throws Exception {
parse(new UserService());
}
}
5. 编译期注解解析(APT)
除了运行期反射解析,还有编译期注解处理(Annotation Processing Tool, APT):
- 原理:在 Java 编译阶段,通过继承
AbstractProcessor自定义注解处理器,扫描源码中的注解,自动生成代码(如 Lombok 的@Data、MyBatis 的@Mapper)。 - 特点:运行在编译期,不影响运行期性能,生成的代码和手写代码完全一致。
三、Java 注解的作用域(@Retention)
核心回答
注解的作用域由元注解 @Retention 控制,它指定了注解信息的保留阶段,共 3 种取值,对应 3 个生命周期:
| 取值(RetentionPolicy) | 保留阶段 | 作用 | 常见场景 |
|---|---|---|---|
SOURCE |
源码阶段 | 注解仅保留在 .java 源码中,编译成 .class 字节码时会被丢弃 |
编译期检查(如 @Override、@SuppressWarnings)、Lombok 注解、APT 生成代码 |
CLASS |
字节码阶段 | 注解保留在 .class 字节码中,但 JVM 加载类时会被丢弃,运行期无法通过反射获取 |
字节码增强(如字节码插桩、ASM 框架)、编译期校验 |
RUNTIME |
运行期 | 注解保留在字节码中,JVM 加载类后依然存在,可以通过反射在运行期获取 | Spring 框架注解(@Controller、@Service)、自定义业务注解、AOP 切面 |
补充考点
- 默认值 :如果不写
@Retention,默认是CLASS级别,运行期无法通过反射获取注解,这是新手常见坑! - 作用域优先级 :
RUNTIME>CLASS>SOURCE,只有RUNTIME级别的注解才能在运行期被反射读取,是自定义业务注解的唯一选择。 - 元注解的作用域 :元注解(如
@Target、@Retention本身)的作用域是RUNTIME,因为需要在运行期被解析。
四、自定义注解完整实战(面试必写)
1. 自定义注解的 4 个核心元注解
自定义注解必须配合**元注解(修饰注解的注解)**使用,核心 4 个:
| 元注解 | 作用 | 取值 |
|---|---|---|
@Target |
指定注解可以标记的元素类型(作用范围) | ElementType.TYPE(类/接口)、METHOD(方法)、FIELD(字段)、PARAMETER(参数)、CONSTRUCTOR(构造器)、ANNOTATION_TYPE(注解)等 |
@Retention |
指定注解的保留阶段(作用域) | RetentionPolicy.SOURCE/CLASS/RUNTIME |
@Documented |
指定注解是否会被包含在 Javadoc 中 | 无取值,标记即可 |
@Inherited |
指定注解是否可以被子类继承 | 无取值,标记即可(仅对类注解生效,方法/字段注解不继承) |
2. 完整自定义注解示例(日志注解)
java
import java.lang.annotation.*;
/**
* 自定义日志注解:标记需要记录日志的方法
*/
@Target(ElementType.METHOD) // 只能标记在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行期保留,可通过反射获取
@Documented // 包含在Javadoc中
@Inherited // 子类可继承(方法注解不生效,仅类注解生效)
public @interface OperationLog {
// 注解属性:方法返回值类型 + 属性名() + [default 默认值]
String value() default ""; // 操作描述
String type() default "QUERY"; // 操作类型,默认查询
boolean saveDb() default true; // 是否入库,默认是
}
3. 注解属性的规则
- 注解属性的类型只能是:基本数据类型、String、Class、枚举、注解、以上类型的数组。
- 如果属性没有
default默认值,使用注解时必须给属性赋值;有默认值可以不赋值。 - 如果注解只有一个属性,且属性名为
value,使用注解时可以省略value=,直接写值(如@OperationLog("查询用户"))。 - 数组属性的赋值:
@OperationLog(value = {"a","b"})。
4. 注解解析器(AOP 实现,Spring 场景)
在 Spring 中,通常结合 AOP 实现注解的逻辑增强,这是面试高频考点:
java
@Aspect
@Component
public class OperationLogAspect {
@Around("@annotation(operationLog)") // 切点:匹配标记了@OperationLog的方法
public Object around(ProceedingJoinPoint joinPoint, OperationLog operationLog) throws Throwable {
// 1. 读取注解属性
String value = operationLog.value();
String type = operationLog.type();
boolean saveDb = operationLog.saveDb();
// 2. 方法执行前:记录日志
System.out.println("【前置日志】操作类型:" + type + ",描述:" + value);
// 3. 执行目标方法
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
// 4. 方法执行后:记录耗时、入库
System.out.println("【后置日志】执行耗时:" + (end - start) + "ms");
if (saveDb) {
System.out.println("日志入库成功");
}
return result;
}
}
五、高频面试题补充
1. 注解和注释的区别?
- 注释:给程序员看的,用于代码说明,编译后完全丢弃,不影响代码运行。
- 注解:给程序看的,是元数据,会保留在源码/字节码/运行期,可被程序读取,影响程序逻辑。
2. 如何实现自定义注解的继承?
- 给注解添加
@Inherited元注解,仅对类注解生效:父类标记了注解,子类会自动继承该注解;方法、字段、接口上的注解不会被继承。 - 方法注解的继承:需要通过反射遍历父类的方法,手动实现继承逻辑。
3. 注解可以继承吗?
- 注解本身不能继承其他注解 ,也不能被其他类继承,只能被
@Inherited标记实现子类继承注解。 - 所有注解都隐式继承了
java.lang.annotation.Annotation接口,这是 JVM 自动完成的。
4. 什么是元注解?常见的元注解有哪些?
元注解是修饰注解的注解,用于定义注解的规则,JDK 内置 4 个核心元注解(上面已讲),Java 8+ 新增 2 个:
@Repeatable:允许同一个注解在同一个元素上多次使用。@Native:标记字段可以被本地代码引用。
5. Spring 中的注解是如何工作的?
Spring 中的注解(如 @Controller、@Autowired)本质是自定义注解 + 反射解析 + AOP 增强:
- Spring 启动时,扫描所有类,通过反射读取类、方法上的注解。
- 根据注解的类型,执行对应的逻辑:如
@Controller注册 Bean 到 Spring 容器,@Autowired完成依赖注入。 - 结合 AOP 实现注解的逻辑增强,如
@Transactional实现事务管理。
六、面试答题技巧
- 原理题:先讲"注解本质是继承 Annotation 的接口",再讲"通过反射+动态代理解析",最后补代码示例。
- 作用域题:分 3 个级别讲清楚保留阶段和适用场景,强调默认值是 CLASS,运行期必须用 RUNTIME。
- 自定义注解题:先讲 4 个核心元注解,再写完整的自定义注解+解析代码,最后结合 Spring AOP 讲实际应用。
- 加分项:提到 APT 编译期注解、动态代理原理、Spring 注解的实现,体现对框架底层的理解。