← 返回题目列表

Java 注解是什么?注解的原理是怎样的?如何自定义注解?

高频 中等 第 16 / 32 题 更新于 2026/07/26
注解元注解反射Annotation

简化版

注解(Annotation)是给代码贴的「元数据标签」,本身不改变程序逻辑,而是供编译器、框架、工具在编译期或运行期读取后做相应处理。它的原理是:注解本质是一种特殊接口@interface),加了注解的元素在运行期可以通过反射读取注解信息,框架据此做事(如 Spring 看到 @Autowired 就注入 Bean)。自定义注解要用四个元注解配置它:@Target(能贴在哪)、@Retention(保留到哪个阶段)、@Documented@Inherited

详细版

注解的三个保留级别@Retention 决定):

级别保留到能否反射读取例子
SOURCE只在源码,编译后丢弃@Override@SuppressWarnings(给编译器看)
CLASS(默认)保留到 class 文件,运行时不加载字节码增强工具用
RUNTIME保留到运行期,可反射读取@Autowired@RequestMapping(框架运行时用)

四个元注解(用来注解「注解」的注解):

元注解作用
@Target限定注解能用在哪(类/方法/字段/参数…),如 @Target(ElementType.METHOD)
@Retention保留级别(SOURCE/CLASS/RUNTIME)
@Documented是否出现在 Javadoc
@Inherited子类是否继承父类的这个注解

自定义注解 + 反射读取

// 1. 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)   // 必须 RUNTIME 才能反射读
public @interface LogTime {
    String value() default "";        // 注解的"属性",形似方法
}

// 2. 使用
public class OrderService {
    @LogTime("下单")
    public void createOrder() { ... }
}

// 3. 反射读取(框架/AOP 就是这么干的)
Method m = OrderService.class.getMethod("createOrder");
if (m.isAnnotationPresent(LogTime.class)) {
    LogTime anno = m.getAnnotation(LogTime.class);
    System.out.println(anno.value());   // "下单"
    // 在这里插入计时逻辑...
}

⚠️ 注解本身不做任何事——它只是「贴了个标签」。真正干活的是读取注解的代码(编译器 / 框架的反射处理器 / 字节码工具)。没有处理器,注解就是一堆没人理会的元数据。这是理解注解最关键的一点。

完整版教学

一、注解的本质:不改变逻辑的「元数据」

注解是「关于代码的数据」(metadata)。它不参与业务运算,而是给代码附加一些「说明」,让别的程序读了之后做决定:

@Override      → 告诉编译器"这是重写",编译器帮你检查父类真有这方法
@Deprecated    → 告诉编译器/IDE"这个已过时",用了就警告
@Autowired     → 告诉 Spring"这里要注入一个 Bean"
@Transactional → 告诉 Spring"这个方法要开事务"

关键认知:注解自己什么都不做@Transactional 不会自己开启事务——是 Spring 的 AOP 在运行时用反射看到这个注解,才去织入事务逻辑。所以注解 = 「标签」 + 「读标签的处理器」两部分,缺一不可。很多人以为「加个 @Transactional 就有事务了」,其实是 Spring 在背后读注解干活。

二、注解的原理:它是一种特殊接口

@interface 定义的注解,编译后本质是一个继承 java.lang.annotation.Annotation 的接口

public @interface LogTime { String value(); }
// 编译后近似等价于:
public interface LogTime extends Annotation {
    String value();   // 注解的"属性"其实是接口的抽象方法
}

所以注解里的「属性」写起来像方法(String value()),因为它本来就是方法。当你写 @LogTime("下单"),JVM 会为这个注解生成一个动态代理实例,调 anno.value() 就返回 “下单”。这解释了为什么注解属性用 () 且能有 default 值——它们是接口方法。理解「注解是接口、属性是方法」,就看懂了注解的底层。

三、四个元注解:怎么定制一个注解

自定义注解时,用「元注解」(注解注解的注解)来配置它的行为,最重要的是 @Target@Retention

@Target({ElementType.METHOD, ElementType.TYPE})  // 能贴在方法和类上
@Retention(RetentionPolicy.RUNTIME)              // 运行时可反射读
@Documented                                       // 进 Javadoc
@Inherited                                        // 子类继承
public @interface MyAnno { }
  • @Target:限定使用位置(METHOD/TYPE/FIELD/PARAMETER/…),贴错位置编译报错。
  • @Retention:保留级别,决定注解「活多久」,是最关键的一个(见下节)。
  • @Documented:让注解出现在生成的 Javadoc 里。
  • @Inherited:子类能否继承父类上的这个注解(默认不继承,且只对类注解有效,方法注解无效)。

四、@Retention 三级别:为什么框架注解必须 RUNTIME

保留级别决定注解能活到哪个阶段,也决定「谁能读它」:

源码 ──编译──▶ class 文件 ──类加载──▶ 运行期(JVM 内存)
 │              │                      │
SOURCE         CLASS                 RUNTIME
(编译后丢弃)   (在 class 里,不加载)  (运行时可反射读)

@Override(SOURCE):编译器检查完就没用了,不进 class
字节码工具注解(CLASS):留在 class 供工具处理,但 JVM 加载时不保留
@Autowired(RUNTIME):Spring 运行时要用反射读它,必须保留到运行期

核心结论:凡是要靠反射在运行时读取的注解(几乎所有框架注解),必须是 RUNTIME。如果你自定义注解想让框架/AOP 读取,却忘了写 @Retention(RUNTIME)(默认是 CLASS),反射会读不到,注解「形同虚设」——这是自定义注解最常见的坑。

五、注解怎么被「用起来」:三种处理方式

注解本身是死的,靠三类处理器让它活起来:

处理时机处理者例子
编译期编译器 / 注解处理器(APT)Lombok 的 @Data 编译期生成 getter/setter;@Override 检查
类加载/字节码期字节码增强工具一些 AOP、字节码织入框架
运行期反射Spring 读 @Autowired、JUnit 读 @Test、MyBatis 读 @Select

以运行期反射为例,框架的套路都一样:扫描类/方法 → isAnnotationPresent 判断有没有注解 → getAnnotation 取出注解属性 → 据此执行逻辑。比如一个简单的接口耗时监控:扫描所有方法,看到 @LogTime 就用 AOP 在方法前后加计时。Lombok 则走编译期——它是个注解处理器,在编译时读到 @Data 就往字节码里生成 getter/setter/toString,所以运行时你看不到这些方法的源码但能调用。

六、注解 vs XML 配置:为什么注解成了主流

早期框架用 XML 配置(如 Spring 的 applicationContext.xml),后来注解逐渐取代它:

维度注解XML
位置和代码在一起,直观独立文件,配置与代码分离
类型安全编译期检查字符串,运行时才报错
修改成本改代码要重新编译改 XML 不用编译
集中管理分散在各处一处总览全部配置

注解的「就近声明、类型安全」让它在大多数场景胜出(@Service@RequestMapping 比一堆 XML 清爽);但需要「集中管理、免编译修改」的场景(如数据源、第三方类的配置)XML 或配置文件仍有价值。现代框架多是「注解为主 + 配置文件为辅」。

记忆钩子:「注解是元数据标签,本质是继承 Annotation 的接口;自己不做事,靠处理器(编译器/反射/字节码)读了才干活;四元注解定制它,@Retention(RUNTIME) 是框架注解的命根子」

七、常见误区与追问

  • 误区:加了注解就会自动生效。 注解只是标签,必须有对应的处理器(编译器/反射/字节码工具)读取它才有作用;没处理器的注解毫无效果。
  • 误区:所有注解运行时都能反射读到。 只有 RUNTIME 级别能反射读;SOURCE(如 @Override)编译后就没了,CLASS(默认)运行时不加载。
  • 误区:自定义注解不写 @Retention 也能被框架读。 默认是 CLASS,反射读不到;要被框架运行时读取必须显式 @Retention(RUNTIME)
  • 误区:@Inherited 能让子类继承父类方法上的注解。 只对类上的注解有效,且只是「子类查得到父类的类注解」;方法注解不被继承。
  • 追问:注解的属性为什么写成方法形式? 因为注解本质是接口,属性就是接口的抽象方法,value() 是方法,default 是默认返回值;用 @Anno(x=1) 赋值。
  • 追问:Lombok 的 @Data 是怎么工作的? 它是编译期注解处理器(APT),编译时读到 @Data 就往字节码里生成 getter/setter/equals/toString,属于编译期处理而非运行时反射。
  • 追问:Spring 是如何处理 @Autowired 的? 通过 BeanPostProcessor(AutowiredAnnotationBeanPostProcessor),在 Bean 初始化时反射扫描字段/方法上的 @Autowired,找到对应 Bean 注入进去。

八、加强记忆

注解是给代码贴的元数据标签,本身不改变逻辑,核心心法是「注解 = 标签 + 处理器」——@Transactional 自己不开事务,是 Spring 反射读到它才织入事务,没有处理器的注解毫无作用。原理上,注解本质是继承 Annotation 的特殊接口,属性就是接口的抽象方法(所以写成 value() 形式、能有 default),使用时 JVM 生成动态代理实例返回属性值。自定义注解靠四个元注解@Target(贴哪)、@Retention(活多久)、@Documented@Inherited,其中 @Retention(RUNTIME)框架注解的命根子——不写默认 CLASS,反射就读不到、注解形同虚设。注解的处理分三档:编译期(Lombok/@Override)、字节码期、运行期反射(Spring/JUnit/MyBatis,套路是「扫描→isAnnotationPresent→getAnnotation→干活」)。相比 XML,注解就近声明、类型安全,成了主流。一句话「注解是元数据接口、属性即方法、靠处理器生效、RUNTIME 才能被反射读」。