Java 注解是什么?注解的原理是怎样的?如何自定义注解?
简化版
注解(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 才能被反射读」。