Skip to content
Go back

注解原理——@Override背后有一个动态代理

注解原理:@Override 背后是 JDK 动态代理

一句话结论(30s)

注解本质是继承 Annotation 的接口且没有实现类,因为运行时靠 JDK 动态代理把 class 文件属性表里的值映射为方法返回值。关键设计是 AnnotationInvocationHandler 按方法名直接从 Map 取值、根本不调 method.invoke();代价是只有 RetentionPolicy.RUNTIME 才走这套运行时机制,Lombok 用 SOURCE 在编译期改 AST 则完全不需要代理。

核心原理(2min)

javac 把 RUNTIME 注解写入 RuntimeVisibleAnnotations 属性表,运行时 AnnotationParser 解析成 Map<String,Object> memberValues,再用 Proxy.newProxyInstance + AnnotationInvocationHandler 创建注解实例;调用 anno.value() 时 handler 直接 memberValues.get("value"),为空再取 method.getDefaultValue()

底层深入(5-10min)

注解的本质

public @interface MyAnnotation {
    String value() default "default";
}

// javac 编译后,MyAnnotation 本质上相当于:
public interface MyAnnotation extends java.lang.annotation.Annotation {
    String value();
}

注解是继承了 java.lang.annotation.Annotation 的接口。没有实现类。

💭 思考:注解既然是接口又没实现类,那调用 anno.value() 怎么会返回具体值?这就矛盾了——接口方法没实现怎么调用?答案在动态代理:运行时用 Proxy 生成一个实现该接口的代理对象,把方法调用拦截下来,从 Map 里取值返回。

运行时解析

// AnnotationParser 从 class 文件的 RuntimeVisibleAnnotations 属性
// 解析出 Map<String, Object> memberValues:
Map<String, Object> memberValues = new HashMap<>();
memberValues.put("value", "hello");  // 从 class 文件属性表中读出来的

// 然后用 JDK 动态代理创建注解实例
MyAnnotation anno = (MyAnnotation) Proxy.newProxyInstance(
    MyAnnotation.class.getClassLoader(),
    new Class[]{MyAnnotation.class},
    new AnnotationInvocationHandler(MyAnnotation.class, memberValues)
);

AnnotationInvocationHandler 的特殊之处

普通业务代理的 invoke() 最终会 method.invoke(target, args) 调用真实实现类的方法。但注解没有实现类

// AnnotationInvocationHandler.invoke()
public Object invoke(Object proxy, Method method, Object[] args) {
    String member = method.getName();
    // anno.value() → member = "value"
    Object result = memberValues.get(member);  // 直接从 Map 取值!
    if (result == null) {
        // 有 default 值返回 default,没有抛异常
        result = method.getDefaultValue();
    }
    return result;
}

invoke() 根本不调 method.invoke()——直接按方法名从 Map 取值返回。 这就是”注解没有实现类但方法有返回值”的原因——值来自 class 文件解析出的 memberValues Map。

💭 思考:AnnotationInvocationHandler 和普通业务代理最大的区别是什么?普通代理 invoke 最终要 method.invoke 调真实对象;注解没真实对象,所以 handler 直接按方法名从 memberValues Map 取值。这就是「注解方法有返回值却没有实现类」的真相。

注解属性在 class 文件中存在哪里?

javap -v MyClass.class
...
RuntimeVisibleAnnotations:
  #12 @MyAnnotation(#13 value="hello")

RuntimeVisibleAnnotations 属性表中存储了注解的全限定名和每个属性值。只有 RetentionPolicy.RUNTIME 的注解才会写入 RuntimeVisibleAnnotations

💭 思考:注解的「值」到底存在哪?存在 class 文件的 RuntimeVisibleAnnotations 属性表里,key-value 形式。运行时 AnnotationParser 读出来放进 Map,代理对象取值时直接从 Map 拿。所以注解值 = 编译期写进 class 文件 + 运行期读进 Map。

Lombok 为什么不需要运行时注解?

Lombok 的 @Data@GetterRetentionPolicySOURCE——编译期就给丢掉了。Lombok 在 javac 的 AST 阶段(Annotation Processing)直接修改语法树,插入 getter/setter 等方法的字节码节点。生成的 class 文件里根本没有 @Data 注解,自然也不需要运行时动态代理。

Lombok 用 @Retention(SOURCE) 是对的——这些注解在运行期没有意义(方法已经生成了),去掉后减小 class 文件体积。

💭 思考:为什么 Lombok 的注解不需要运行时代理?因为它的 RetentionPolicy 是 SOURCE,编译期就丢弃了——Lombok 在 javac 的 AST 阶段直接改语法树塞进 getter/setter 字节码,class 文件里根本没这个注解,自然不用代理。一个改 AST、一个留到运行时,两条路线。

总结

注解 = 接口(extends Annotation)+ class 文件属性表(存储属性值)+ JDK 动态代理(提供运行时实例)+ AnnotationInvocationHandler(按方法名从 Map 取属性值)。值从哪来?class 文件的 RuntimeVisibleAnnotations 属性表 → AnnotationParser 解析 → Map<String, Object>

章末提问

追问 1:注解的本质是什么?为什么它「没有实现类」却能返回值?

结论先行:注解本质是继承 Annotation 的接口,运行时靠 JDK 动态代理生成实现该接口的实例,AnnotationInvocationHandler 按方法名从 Map 取值返回。

因为:javac 把 RUNTIME 注解的 key-value 写进 class 文件的 RuntimeVisibleAnnotations 属性表,运行时 AnnotationParser 解析成 Map,再用 Proxy.newProxyInstance + AnnotationInvocationHandler 创建代理对象。调用 anno.value() 时 handler 拦截,直接从 memberValues 取 key=“value” 的项,所以无需实现类。

追问 2:AnnotationInvocationHandler 与普通动态代理的 invoke 有什么本质区别?

结论先行:普通代理的 invoke 最终调 method.invoke 执行真实实现类方法,而注解代理根本不调 method.invoke,直接按方法名从 Map 取值。

因为:注解没有实现类、没有可调用的真实对象,它的值只存在于 class 文件解析出的 memberValues Map。所以 handler 取 memberValues.get(method.getName()),为空再取 method.getDefaultValue(),这决定了注解属性的读取是一个查表操作。

追问 3:Lombok 的 @Data 为什么不需要运行时注解机制?

结论先行:因为 @Data 的 RetentionPolicy 是 SOURCE,编译期就丢弃,Lombok 在 javac 的 AST 阶段直接修改语法树插入方法,class 文件里根本没有 @Data。

因为:@Data 的作用只是「触发代码生成」,方法生成完注解就没意义了,标 SOURCE 反而减小 class 体积、省去运行时代理。这跟 @Override 等需要运行时反射读取的注解走的是两条完全不同的路线。


Share this post on:

Previous Post
ArrayList与CopyOnWriteArrayList——1.5倍扩容与fail-fast机制
Next Post
Java泛型类型擦除——编译器为什么要生成桥接方法?