前言:
注解是 Java 里的特殊标记。它可以写在类、方法、字段、参数等位置,然后被编译器、框架或其他程序读取。
学习注解时,不能只停在"怎么写"。更关键的是看三件事:这个注解怎么定义、能写在哪里、谁来读取它。本篇结合 day06 中的 demo3annotation 示例,把自定义注解、元注解和反射解析串起来。
一、注解到底是什么
注解本质上和 Annotation 接口有关。我们自己定义注解时,不是写 class,也不是写 interface,而是使用 @interface。
一个最简单的自定义注解可以这样写:
java
public @interface Mybook {
String name();
int age() default 18;
String[] address();
}
这里的 name、age、address 不是普通方法调用,而是注解的属性。使用注解时,需要给没有默认值的属性赋值:
java
@Mybook(name = "小赵", age = 23, address = {"广州", "北京"})
public class AnnotationDemo1 {
}
age 有默认值,所以使用时可以不写;name 和 address 没有默认值,就必须提供。
还有一种常见简写:如果注解里只有一个核心属性叫 value,使用时可以省略属性名。
java
public @interface A {
String value();
String hobby() default "吃饭";
}
@A("hello")
public void test() {
}
这就是很多框架注解看起来很简洁的原因,并不是语法特殊,而是 value 属性触发了简写规则。
二、元注解:限制注解怎么用
自己定义出来的注解,默认还不够完整。它能写在类上、方法上,还是字段上?它会保留到源码阶段、字节码阶段,还是运行时?这些要靠元注解控制。
常见的两个元注解是 @Target 和 @Retention。
java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface A2test2 {
}
Target 用来限制注解能写的位置。上面这个注解只能写在类、接口或成员变量上,不能写在方法、构造器、局部变量上。
常见取值可以这样记:
| 取值 | 能写的位置 |
|---|---|
| TYPE | 类、接口 |
| FIELD | 成员变量 |
| METHOD | 成员方法 |
| PARAMETER | 方法参数 |
| CONSTRUCTOR | 构造器 |
| LOCAL_VARIABLE | 局部变量 |
Retention 用来限制注解保留到哪个阶段。示例中写的是 RetentionPolicy.RUNTIME,意思是运行时还存在,所以后面才能通过反射读取。
如果没有运行时保留,注解可能写得上去,但程序运行时读不到。很多同学第一次解析注解失败,问题就出在这里。
三、类上和方法上都可以写注解
在 day06 的示例中,Mytest3 同时允许写在类和方法上:
java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Mytest3 {
String value();
double height() default 170;
String[] address();
}
所以它既可以标记类:
java
@Mytest3(value = "张若楠", address = {"广州", "深圳"})
public class Demo3 {
}
也可以标记方法:
java
public class Demo3 {
@Mytest3(value = "杨幂", address = {"北京", "石家庄"})
public void go() {
}
}
这时注解只是"贴上去了"。它还不会自动执行任何逻辑。要让注解产生效果,必须有其他程序去读取它。
四、用反射解析类上的注解
解析注解通常会结合反射完成。类、方法、字段等结构都可以通过反射拿到,然后判断它们身上有没有某个注解。
下面是解析类上注解的核心代码:
java
import java.util.Arrays;
public class AnnotationDemo3 {
public void parseClass() {
Class<?> c1 = Demo3.class;
if (c1.isAnnotationPresent(Mytest3.class)) {
Mytest3 mytest3 = c1.getDeclaredAnnotation(Mytest3.class);
String name = mytest3.value();
double height = mytest3.height();
String[] address = mytest3.address();
System.out.println(name);
System.out.println(height);
System.out.println(Arrays.toString(address));
}
}
}
这里先通过 Class 拿到 Demo3 的类对象,再判断这个类上有没有 Mytest3 注解。判断存在后,再取出注解对象,读取里面的 value、height、address。
这一步可以理解为:注解负责做标记,反射负责找到标记,程序再根据标记里的属性继续处理。
五、用反射解析方法上的注解
方法上的注解也一样,只是要先拿到 Method 对象。
java
import java.lang.reflect.Method;
import java.util.Arrays;
public class AnnotationDemo3 {
public void parseMethod() throws Exception {
Class<?> c1 = Demo3.class;
Method method = c1.getMethod("go");
if (method.isAnnotationPresent(Mytest3.class)) {
Mytest3 mytest3 = method.getDeclaredAnnotation(Mytest3.class);
String name = mytest3.value();
double height = mytest3.height();
String[] address = mytest3.address();
System.out.println(name);
System.out.println(height);
System.out.println(Arrays.toString(address));
}
}
}
这段代码和解析类的思路完全一样,只是目标从类换成了方法。先找到 go 方法,再判断方法上是否存在 Mytest3,最后读取注解属性。
所以注解解析的固定套路可以总结成四步:拿到目标对象、判断是否有注解、获取注解对象、读取注解属性。
六、模拟 JUnit:框架为什么喜欢用注解
注解真正有价值的地方,是可以让框架根据标记自动做事。day06 里用 Mytest4 模拟了一个简单的测试框架:
java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Mytest4 {
int count() default 1;
}
这个注解只能写在方法上,并且提供了一个 count 属性,用来控制方法执行次数。
java
import java.lang.reflect.Method;
public class AnnotationDemo4 {
public static void main(String[] args) throws Exception {
AnnotationDemo4 ad = new AnnotationDemo4();
Class<?> c1 = AnnotationDemo4.class;
Method[] methods = c1.getMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(Mytest4.class)) {
Mytest4 mytest4 = method.getDeclaredAnnotation(Mytest4.class);
int count = mytest4.count();
for (int i = 0; i < count; i++) {
method.invoke(ad);
}
}
}
}
@Mytest4
public void test1() {
System.out.println("test1 方法执行了");
}
@Mytest4(count = 3)
public void test3() {
System.out.println("test3 方法执行了");
}
}
这段代码的核心逻辑很清楚:先拿到当前类中的所有方法,遍历每个方法;如果方法上有 Mytest4,就读取 count 属性;最后通过反射调用这个方法。
这就是注解的典型应用场景。方法本身没有主动注册,框架也不需要写死方法名,只要扫描注解,就知道哪些方法需要执行。
七、常见问题和排查思路
第一个问题是注解写了,但反射读不到。先检查 Retention 是否设置为运行时保留。只有运行时还能存在的注解,程序运行时才有机会读取。
第二个问题是注解不能写到某个位置。先看 Target 有没有包含对应位置。比如只允许写在方法上的注解,就不能写在类或字段上。
第三个问题是以为写了注解就会自动执行。注解本身只是一种标记,它不会主动调用方法,也不会自动产生业务逻辑。真正让它生效的,是后面那段扫描、判断、读取、执行的程序。
第四个问题是 value 简写用错。只有属性名叫 value,并且其他属性都有默认值时,才适合直接写一个值。如果还有其他没有默认值的属性,就必须写完整的属性名。
八、可以怎么验证
如果想验证类和方法上的注解解析,可以运行 AnnotationDemo3 中解析类、解析方法的代码。预期现象是能打印出注解里的 name、height 和 address。
如果想验证注解驱动执行,可以运行 AnnotationDemo4 的 main 方法。预期现象是只有标了 Mytest4 的方法会执行;没有标记的方法不会执行;count 写成 3 的方法会执行 3 次。
如果结果不符合预期,不要先怀疑反射代码复杂,先按顺序查三件事:注解有没有写到目标位置、元注解限制是否正确、运行时是否真的扫描到了这个类或方法。
总结
Java 注解可以理解为"写给程序看的标记"。定义注解只是第一步,元注解决定它能贴在哪里、能保留多久,反射解析决定它能不能真正被程序读取。
以后再看到框架里的各种注解,可以按同一个思路拆:这个注解标记了什么,框架在什么时候扫描它,扫描后根据哪些属性做处理。能顺着这条链路看,注解就不会只是背概念,而是能看懂框架运行逻辑的一把入口。