跳至主要內容

hint - 注解理解3

codejava hint约 889 字大约 3 分钟

注解3

自定义注解流程

自定义注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Penguin {
    String value() default "";
}

测试类

然后在 Main.java 使用

import com.ekko.annotation.Penguin;

public class Main {
    @Penguin("123")
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

编译

编译 Penguin.java 时,javac 看到它上面有 @Target@Retention,会依次做这些事:

javac 编译 Penguin.java
        ↓
发现 @Target({ElementType.TYPE, ElementType.METHOD})
        ↓
去 classpath 找 Target.class,加载进内存,提取两类信息:
  ① value() → ElementType[]            (用来校验你传的值类型合不合法)
  ② Target 自身的 @Retention(RUNTIME)  (说明 Target 运行时可见)
        ↓
发现 @Retention(RetentionPolicy.RUNTIME)
        ↓
去 classpath 找 Retention.class,同样提取信息
        ↓
校验通过,将注解信息写入 Penguin.class
  - 设置 ACC_ANNOTATION | ACC_INTERFACE 标志位
  - 隐式添加 implements java.lang.annotation.Annotation
  - 在 RuntimeVisibleAnnotations 属性中记录 @Target 和 @Retention 的值

javap -v Penguin.class 可以看到生成结果:

public interface com.ekko.annotation.Penguin extends java.lang.annotation.Annotation
  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION

  public abstract java.lang.String value();

RuntimeVisibleAnnotations:
  0: java.lang.annotation.Target(
       value=[TYPE, METHOD]
     )
  1: java.lang.annotation.Retention(
       value=RUNTIME
     )

编译 Main.java 时,javac 看到 @Penguin("123") 用在了 main 方法上:

发现 @Penguin("123") 标注在方法上
        ↓
加载 Penguin.class,读取它的 @Target → 允许 METHOD ✓
        ↓
读取 value() 的返回类型 → String,检查传入的 "123" 类型是否匹配 ✓
        ↓
读取 @Retention(RUNTIME) → 需要写入 .class 且运行时可见
        ↓
将注解信息写入 Main.class 的 RuntimeVisibleAnnotations 属性

运行时读取

注解标注了 @Retention(RetentionPolicy.RUNTIME),所以可以在运行时通过反射读取:

import com.ekko.annotation.Penguin;
import java.lang.reflect.Method;

public class Main {
    @Penguin("123")
    public static void main(String[] args) throws Exception {
        Method method = Main.class.getMethod("main", String[].class);

        if (method.isAnnotationPresent(Penguin.class)) {
            Penguin penguin = method.getAnnotation(Penguin.class);
            System.out.println(penguin.value()); // 输出: 123
        }
    }
}

反射读取流程:

method.getAnnotation(Penguin.class)
        ↓
JVM 去 Main.class 的 RuntimeVisibleAnnotations 属性里查找
        ↓
找到 @Penguin,value = "123"
        ↓
JVM 动态生成一个实现了 Penguin 接口的代理对象返回
        ↓
调用 penguin.value() → 返回 "123"

SOURCE 级别

SOURCE 级注解在编译后就被丢弃,它靠的是 APT(Annotation Processing Tool,注解处理器) 在编译期处理,典型代表是 Lombok 的 @Data@Getter

你的 .java 源文件
        ↓
javac 启动,先运行所有注册的注解处理器(APT)
        ↓
APT 读取 AST(抽象语法树),发现 @Data 注解
        ↓
APT 自动生成 getter/setter/toString 等方法,插入 AST
        ↓
javac 继续编译(此时 AST 里已经有生成的代码了)
        ↓
输出 .class 文件(@Data 注解本身不写入)

自己写一个最简单的 APT,在编译时打印所有被 @Penguin 标注的方法:

import javax.annotation.processing.*;
import javax.lang.model.element.*;
import java.util.Set;

@SupportedAnnotationTypes("com.ekko.annotation.Penguin")
@SupportedSourceVersion(javax.lang.model.SourceVersion.RELEASE_17)
public class PenguinProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(Penguin.class)) {
            processingEnv.getMessager().printMessage(
                javax.tools.Diagnostic.Kind.NOTE,
                "发现 @Penguin 标注的元素: " + element.getSimpleName()
            );
        }
        return true;
    }
}

编译时会看到:

注意: 发现 @Penguin 标注的元素: main

CLASS 级别

CLASS 注解会写入 .class 文件,但 JVM 加载时不读入内存,无法用反射获取。它的使用者是字节码操作工具,在 .class 文件加载进 JVM 之前直接修改字节码。

.class 文件(含 CLASS 级注解)
        ↓
字节码工具(ASM / Javassist)读取
        ↓
发现注解标记,按规则修改字节码(插入代码、替换方法体等)
        ↓
修改后的字节码交给 JVM 加载
        ↓
JVM 运行(注解已不可见,效果已生效)

典型代表:AspectJ 的编译期织入

三种处理时机对比

SOURCE + APTCLASS + 字节码工具RUNTIME + 反射
处理时机编译期编译后 / 加载前运行时
处理者注解处理器(APT)ASM / Javassist 等你自己的代码
典型代表Lombok @DataAspectJ 编译期织入Spring @Autowired
性能零运行时开销很低开销有反射开销
注解写入 .class
运行时可见
上次编辑于: