【JAVASE | 第十九篇】Java 注解入门

前言:

注解是 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 注解可以理解为"写给程序看的标记"。定义注解只是第一步,元注解决定它能贴在哪里、能保留多久,反射解析决定它能不能真正被程序读取。

以后再看到框架里的各种注解,可以按同一个思路拆:这个注解标记了什么,框架在什么时候扫描它,扫描后根据哪些属性做处理。能顺着这条链路看,注解就不会只是背概念,而是能看懂框架运行逻辑的一把入口。

相关推荐
布朗克1681 小时前
28 网络编程——Socket、TCP/UDP与HttpClient
java·网络·tcp/ip·udp
二月夜9 小时前
剖析Java正则表达式回溯问题
java·正则表达式
xuhaoyu_cpp_java10 小时前
项目学习(三)分页查询
java·经验分享·笔记·学习
程序员二叉10 小时前
【Java】集合面试全套精讲|HashMap/ArrayList高频考点完整版
java·面试·哈希算法
cfm_291410 小时前
JVM GC垃圾回收初步了解
java·开发语言·jvm
心之伊始10 小时前
LangChain4j RAG 实战:Java 后端如何把本地文档接入 Embedding 检索链路
java·架构·源码分析·csdn
许彰午11 小时前
17_synchronized关键字深度解析
java·开发语言
Xzh042312 小时前
AI Agent 学习路线(Java 后端方向)
java·人工智能·学习
艾利克斯冰13 小时前
Java 设计模式-行为型模式(更新中)
java·开发语言·设计模式