注解
问题
Java 注解的原理是什么?元注解有哪些?如何自定义注解?
答案
注解基础
注解(Annotation)是 Java 5 引入的元数据机制,用于为代码添加额外信息。注解本身不影响程序逻辑,需要通过注解处理器(编译期或运行时)来处理。
内置注解
| 注解 | 用途 |
|---|---|
@Override | 标记方法重写(编译器校验) |
@Deprecated | 标记已过时的 API |
@SuppressWarnings | 抑制编译器警告 |
@FunctionalInterface | 标记函数式接口(JDK 8) |
@SafeVarargs | 抑制泛型可变参数警告 |
元注解
用于注解「注解」的注解:
| 元注解 | 说明 | 常用值 |
|---|---|---|
@Target | 注解可以用在什么地方 | TYPE、METHOD、FIELD、PARAMETER |
@Retention | 注解保留到什么时期 | SOURCE、CLASS、RUNTIME |
@Documented | 是否包含在 Javadoc 中 | — |
@Inherited | 子类是否继承该注解 | — |
@Repeatable | 是否可重复使用(JDK 8) | — |
@Retention 的三个级别:
| 级别 | 说明 | 应用场景 |
|---|---|---|
SOURCE | 只在源码中保留,编译后丢弃 | @Override、Lombok |
CLASS | 保留到 class 文件,但 JVM 不加载 | 字节码工具(默认值) |
RUNTIME | JVM 运行时可通过反射读取 | Spring、MyBatis 等框架注解 |
自定义注解
CustomAnnotation.java
// 自定义注解
@Target(ElementType.METHOD) // 只能用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时可通过反射读取
public @interface RateLimit {
int value() default 100; // 默认每秒 100 次
String key() default ""; // 限流 key
String message() default "请求过于频繁";
}
// 使用注解
public class UserController {
@RateLimit(value = 10, key = "login")
public String login(String username, String password) {
// ...
}
}
// 通过反射读取注解
Method method = UserController.class.getMethod("login", String.class, String.class);
if (method.isAnnotationPresent(RateLimit.class)) {
RateLimit limit = method.getAnnotation(RateLimit.class);
System.out.println("限流阈值: " + limit.value()); // 10
System.out.println("限流 key: " + limit.key()); // "login"
}
Spring 中的注解原理
Spring 大量使用注解,核心处理方式包括:
- 运行时反射:通过
Method.getAnnotation()读取注解 - AOP 代理:如
@Transactional,通过动态代理在方法前后加入事务逻辑 - BeanPostProcessor:在 Bean 初始化过程中处理注解,如
@Autowired
Spring 注解处理简化流程
// Spring 处理 @Autowired 的简化流程
// 1. AutowiredAnnotationBeanPostProcessor 扫描 Bean 的字段/方法
// 2. 找到 @Autowired 注解的字段
// 3. 从 IoC 容器中查找匹配的 Bean
// 4. 通过反射 field.set(bean, dependency) 注入
常见面试问题
Q1: 注解的本质是什么?
答案:
注解的本质是一个继承自 java.lang.annotation.Annotation 的特殊接口。使用 @interface 定义的注解,编译器会自动让它继承 Annotation 接口。注解中的属性实际上是接口的抽象方法。
// @interface MyAnnotation 编译后等价于:
public interface MyAnnotation extends Annotation {
String value();
}
运行时通过反射获取注解时,JVM 会通过动态代理为注解接口生成一个实现类。
Q2: @Retention 的三个级别分别在什么场景使用?
答案:
SOURCE:只需要编译期检查或代码生成。如@Override(编译器校验重写)、Lombok 的@Data(编译期生成代码)CLASS:需要在字节码中保留但不需要运行时反射。如一些字节码增强工具需要读取RUNTIME:需要在运行时通过反射读取。如 Spring 的@Component、@Autowired、@Transactional
Q3: 如何实现一个自定义的 Spring 注解?
答案:
以实现一个 @Log 方法日志注解为例:
// 1. 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value() default "";
}
// 2. 创建 AOP 切面处理注解
@Aspect
@Component
public class LogAspect {
@Around("@annotation(log)")
public Object around(ProceedingJoinPoint point, Log log) throws Throwable {
String methodName = point.getSignature().getName();
System.out.println("调用方法: " + methodName + " " + log.value());
long start = System.currentTimeMillis();
Object result = point.proceed();
System.out.println("耗时: " + (System.currentTimeMillis() - start) + "ms");
return result;
}
}
// 3. 使用
@Log("查询用户")
public User findUser(Long id) { ... }
Q4: 注解可以继承吗?
答案:
- 接口层面:注解不能通过
extends继承另一个注解 @Inherited元注解:只作用于类级别注解,当父类标注了带@Inherited的注解时,子类会继承该注解。对方法、字段上的注解无效- Spring 的
@AliasFor:Spring 提供了注解属性别名机制,可以实现类似注解继承的效果(组合注解)
// Spring 组合注解示例
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component // 组合了 @Component
public @interface Service {
@AliasFor(annotation = Component.class)
String value() default "";
}