跳到主要内容

注解

问题

Java 注解的原理是什么?元注解有哪些?如何自定义注解?

答案

注解基础

注解(Annotation)是 Java 5 引入的元数据机制,用于为代码添加额外信息。注解本身不影响程序逻辑,需要通过注解处理器(编译期或运行时)来处理。

内置注解

注解用途
@Override标记方法重写(编译器校验)
@Deprecated标记已过时的 API
@SuppressWarnings抑制编译器警告
@FunctionalInterface标记函数式接口(JDK 8)
@SafeVarargs抑制泛型可变参数警告

元注解

用于注解「注解」的注解:

元注解说明常用值
@Target注解可以用在什么地方TYPEMETHODFIELDPARAMETER
@Retention注解保留到什么时期SOURCECLASSRUNTIME
@Documented是否包含在 Javadoc 中
@Inherited子类是否继承该注解
@Repeatable是否可重复使用(JDK 8)

@Retention 的三个级别:

级别说明应用场景
SOURCE只在源码中保留,编译后丢弃@Override、Lombok
CLASS保留到 class 文件,但 JVM 不加载字节码工具(默认值)
RUNTIMEJVM 运行时可通过反射读取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 大量使用注解,核心处理方式包括:

  1. 运行时反射:通过 Method.getAnnotation() 读取注解
  2. AOP 代理:如 @Transactional,通过动态代理在方法前后加入事务逻辑
  3. 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 "";
}

相关链接