一文读懂注解的底层原理

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 一文读懂注解的底层原理

一、前言

上一篇我们说了注解、元注解以及自定义注解,详情请戳:一文读懂Annotation

注解的声明如下:

{InterfaceModifier} @interface Identifier AnnotationTypeBody

接口修饰符 @interface 注解标识符 注解类型的内容

其中:

  • 注解类型声明中的标识符指定了注解类型的名称。
  • 如果注解类型与它的任何封闭类或接口具有相同的简单名称,则编译时会出现错误。
  • 每个注解类型的直接父接口都是 java.lang.annotation.Annotation。


二、注解的底层实现

我们来自定义一个流控 RateLimit 注解,如下:

@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.TYPE)
public @interface RateLimit {
    int value() default 0;
}

我们用反射获取 RateLimit 注解,然后断点调试观察下反射究竟是怎么样的魔力。

@RateLimit(value = 666)
public class RateLimitMain {
    public static void main(String[] args) {
        RateLimit rateLimit = RateLimitMain.class.getAnnotation(RateLimit.class);
        System.out.println(rateLimit.value());
    }
}

断点调试 @RateLimit 过程中,发现它是一个代理类。

为了验证这一点我们使用 JDK 的反编译命令查看 @RateLimit 的字节码:


javap -c -v RateLimit.class

@RateLimit 反编译后的字节码如下:

Classfile /Users/Riemann/Code/spring-boot-demo/target/classes/com/riemann/springbootdemo/annotation/RateLimit.class
  Last modified 2021-3-23; size 495 bytes
  MD5 checksum 1ce59bd6cc4c7297e8d3f032320b04d3
  Compiled from "RateLimit.java"
public interface com.riemann.springbootdemo.annotation.RateLimit extends java.lang.annotation.Annotation
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
Constant pool:
   #1 = Class              #18            // com/riemann/springbootdemo/annotation/RateLimit
   #2 = Class              #19            // java/lang/Object
   #3 = Class              #20            // java/lang/annotation/Annotation
   #4 = Utf8               value
   #5 = Utf8               ()I
   #6 = Utf8               AnnotationDefault
   #7 = Integer            0
   #8 = Utf8               SourceFile
   #9 = Utf8               RateLimit.java
  #10 = Utf8               RuntimeVisibleAnnotations
  #11 = Utf8               Ljava/lang/annotation/Retention;
  #12 = Utf8               Ljava/lang/annotation/RetentionPolicy;
  #13 = Utf8               RUNTIME
  #14 = Utf8               Ljava/lang/annotation/Documented;
  #15 = Utf8               Ljava/lang/annotation/Target;
  #16 = Utf8               Ljava/lang/annotation/ElementType;
  #17 = Utf8               TYPE
  #18 = Utf8               com/riemann/springbootdemo/annotation/RateLimit
  #19 = Utf8               java/lang/Object
  #20 = Utf8               java/lang/annotation/Annotation
{
  public abstract int value();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_ABSTRACT
    AnnotationDefault:
      default_value: I#7}
SourceFile: "RateLimit.java"
RuntimeVisibleAnnotations:
  0: #11(#4=e#12.#13)
  1: #14()
  2: #15(#4=[e#16.#17])

从上述字节码可以看出:

  • 注解是一个接口,它继承自 java.lang.annotation.Annotation 父接口。
  • @RateLimit 自身定义了一个抽象方法 public abstract int value(); 。


既然注解最后转化为一个接口,注解中定义的注解成员属性会转化为抽象方法,那么最后这些注解成员属性怎么进行赋值的呢?

答案就是:为注解对应的接口生成一个实现该接口的动态代理类。

直接点就是:Java 通过动态代理的方式生成了一个实现了"注解对应接口"的实例,该代理类实例实现了"注解成员属性对应的方法",这个步骤类似于"注解成员属性"的赋值过程,这样子就可以在程序运行的时候通过反射获取到注解的成员属性(这里注解必须是运行时可见的,也就是使用了@Retention(RetentionPolicy.RUNTIME),另外动态代理相关知识这里就不展开讲,留在下一篇讲)。

到这里就解决了上一篇的疑问了,注解的值是如何获取到的,现在恍然大悟。

本着科学严谨的态度,我们还是来瞅一瞅那个代理类是如何获取到注解上的值的。

三、揭秘注解背后的代理类

注解的最底层实现就是一个 JDK 的动态代理类,而这个动态代理类的生成过程在 Debug 面前一览无余。

JDK 中是通过 AnnotatedElement(package java.lang.reflect)接口实现对注解的解析,我们的 Class 类实现了 AnnotatedElement 接口。

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement {
  ...
}

AnnotatedElement 代码:

AnnotatedElement 的注释:

Represents an annotated element of the program currently running in this VM. This interface allows annotations to be read reflectively.

翻译过来就是:

AnnotatedElement 代表了 jvm 中一个正在运行的被注解元素,这个接口允许通过反射的方式读取注解。

可以看下 Class 类中对于 AnnotatedElement 接口都是如何实现的:

我们这就跟着 Debug 来看下 getAnnotation 方法

根据注解的 class 实例从类的注解缓存数据中获取匹配的注解类型

RateLimit 是注解类型,RateLimit.getClass() 获取到的就是 Class 实例

1、Class<?>#getAnnotation(Class<A> annotationClass),通过类型获取注解实例。

public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {
    Objects.requireNonNull(annotationClass);
    return (A) annotationData().annotations.get(annotationClass);
}

2、代码中 annotationData().annotations 是一个 Map(key 为注解的 Class 实例,value 为注解类型)。

// annotation data that might get invalidated when JVM TI RedefineClasses() is called
private static class AnnotationData {
    final Map<Class<? extends Annotation>, Annotation> annotations;
    final Map<Class<? extends Annotation>, Annotation> declaredAnnotations;
    // Value of classRedefinedCount when we created this AnnotationData instance
    final int redefinedCount;
    AnnotationData(Map<Class<? extends Annotation>, Annotation> annotations,
                   Map<Class<? extends Annotation>, Annotation> declaredAnnotations,
                   int redefinedCount) {
        this.annotations = annotations;
        this.declaredAnnotations = declaredAnnotations;
        this.redefinedCount = redefinedCount;
    }
}


3、Class<?>#annotationData(),获取注解的数据。

private AnnotationData annotationData() {
    while (true) { // retry loop
        AnnotationData annotationData = this.annotationData;
        int classRedefinedCount = this.classRedefinedCount;
        if (annotationData != null &&
            annotationData.redefinedCount == classRedefinedCount) {
            return annotationData;
        }
        // null or stale annotationData -> optimistically create new instance
        AnnotationData newAnnotationData = createAnnotationData(classRedefinedCount);
        // try to install it
        if (Atomic.casAnnotationData(this, annotationData, newAnnotationData)) {
            // successfully installed new AnnotationData
            return newAnnotationData;
        }
    }

核心的逻辑是:当 this.annotationData 为空,解析类中的 annotationData 并写入 this.annotationData,最后都会返回 this.annotationData。

4、其中 Atomic.casAnnotationData(this, annotationData, newAnnotationData) 的作用便是将解析到的 annotationData 写入 this.annotationData 。

static <T> boolean casAnnotationData(Class<?> clazz,
                                     AnnotationData oldData,
                                     AnnotationData newData) {
    return unsafe.compareAndSwapObject(clazz, annotationDataOffset, oldData, newData);
}

其中 unsafe.compareAndSwapObject 是一个 native 方法

5、createAnnotationData(classRedefinedCount) 的作用是解析类中用到的 annotationData

private AnnotationData createAnnotationData(int classRedefinedCount) {
    Map<Class<? extends Annotation>, Annotation> declaredAnnotations =
        AnnotationParser.parseAnnotations(getRawAnnotations(), getConstantPool(), this);
    Class<?> superClass = getSuperclass();
    Map<Class<? extends Annotation>, Annotation> annotations = null;
    if (superClass != null) {
        Map<Class<? extends Annotation>, Annotation> superAnnotations =
            superClass.annotationData().annotations;
        for (Map.Entry<Class<? extends Annotation>, Annotation> e : superAnnotations.entrySet()) {
            Class<? extends Annotation> annotationClass = e.getKey();
            if (AnnotationType.getInstance(annotationClass).isInherited()) {
                if (annotations == null) { // lazy construction
                    annotations = new LinkedHashMap<>((Math.max(
                            declaredAnnotations.size(),
                            Math.min(12, declaredAnnotations.size() + superAnnotations.size())
                        ) * 4 + 2) / 3
                    );
                }
                annotations.put(annotationClass, e.getValue());
            }
        }
    }
    if (annotations == null) {
        // no inherited annotations -> share the Map with declaredAnnotations
        annotations = declaredAnnotations;
    } else {
        // at least one inherited annotation -> declared may override inherited
        annotations.putAll(declaredAnnotations);
    }
    return new AnnotationData(annotations, declaredAnnotations, classRedefinedCount);
}

整个的处理逻辑是:

  • 获取类本身的 declaredAnnotations
  • 获取父类的 annotations
  • 将 declaredAnnotations+annotations 整合,返回

6、AnnotationParser#parseAnnotations(byte[] var0, ConstantPool var1, Class<?> var2)

这里已经是 sun 包下的类,这个方法用于解析注解,这一步使用到字节码中常量池的索引解析,常量解析完毕会生成一个成员属性键值对作为下一个环节的入参,常量池的解析可以看AnnotationParser#parseMemberValue方法。

public static Object parseMemberValue(Class<?> var0, ByteBuffer var1, ConstantPool var2, Class<?> var3) {
    Object var4 = null;
    byte var5 = var1.get();
    switch(var5) {
    case 64:
        var4 = parseAnnotation(var1, var2, var3, true);
        break;
    case 91:
        return parseArray(var0, var1, var2, var3);
    case 99:
        var4 = parseClassValue(var1, var2, var3);
        break;
    case 101:
        return parseEnumValue(var0, var1, var2, var3);
    default:
        var4 = parseConst(var5, var1, var2);
    }
    if (!(var4 instanceof ExceptionProxy) && !var0.isInstance(var4)) {
        var4 = new AnnotationTypeMismatchExceptionProxy(var4.getClass() + "[" + var4 + "]");
    }
    return var4;
}

7、AnnotationParser#annotationForMap(final Class<? extends Annotation> var0, final Map<String, Object> var1)

同样是sun.reflect.annotation.AnnotationParser中的方法,用于生成注解的动态代理类。

public static Annotation annotationForMap(final Class<? extends Annotation> var0, final Map<String, Object> var1) {
    return (Annotation)AccessController.doPrivileged(new PrivilegedAction<Annotation>() {
        public Annotation run() {
            return (Annotation)Proxy.newProxyInstance(var0.getClassLoader(), new Class[]{var0}, new AnnotationInvocationHandler(var0, var1));
        }
    });
}

熟悉 JDK 动态代理的这里的代码应该看起来很简单,就是生成一个标准的 JDK 动态代理,而 InvocationHandler 的实例是 AnnotationInvocationHandler,可以看它的成员变量、构造方法和实现 InvocationHandler 接口的 invoke 方法:

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private static final long serialVersionUID = 6182022883658399397L;
    // 保存了当前注解的类型
    private final Class<? extends Annotation> type;
    // 保存了注解的成员属性的名称和值的映射,注解成员属性的名称实际上就对应着接口中抽象方法的名称
    private final Map<String, Object> memberValues;
    private transient volatile Method[] memberMethods = null;
    AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
        Class[] var3 = var1.getInterfaces();
        if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
            this.type = var1;
            this.memberValues = var2;
        } else {
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        }
    }
    public Object invoke(Object var1, Method var2, Object[] var3) {
      // 获取当前执行的方法名称
        String var4 = var2.getName();
        Class[] var5 = var2.getParameterTypes();
        if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
            return this.equalsImpl(var3[0]);
        } else if (var5.length != 0) {
            throw new AssertionError("Too many parameters for an annotation method");
        } else {
            byte var7 = -1;
            switch(var4.hashCode()) {
            case -1776922004:
                if (var4.equals("toString")) {
                    var7 = 0;
                }
                break;
            case 147696667:
                if (var4.equals("hashCode")) {
                    var7 = 1;
                }
                break;
            case 1444986633:
                if (var4.equals("annotationType")) {
                    var7 = 2;
                }
            }
            switch(var7) {
            case 0:
                return this.toStringImpl();
            case 1:
                return this.hashCodeImpl();
            case 2:
                return this.type;
            default:
                // 利用方法名称从memberValues获取成员属性的赋值
                Object var6 = this.memberValues.get(var4);
                if (var6 == null) {
                    throw new IncompleteAnnotationException(this.type, var4);
                } else if (var6 instanceof ExceptionProxy) {
                    throw ((ExceptionProxy)var6).generateException();
                } else {
                    // 这一步就是注解成员属性返回值获取的实际逻辑
                    // 需要判断是否是数组,如果是数组需要克隆一个数组
                    // 不是数组直接返回
                    if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
                        var6 = this.cloneArray(var6);
                    }
                    return var6;
                }
            }
        }
    }
  ...
}

既然知道了注解底层使用了 JDK 原生的 Proxy,那么我们可以直接输出代理类到指定目录去分析代理类的源码,有两种方式可以输出 Proxy 类的源码:

  • 通过 Java 系统属性设置:
    System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");。

  • 通过 -D 参数指定,参数是:
    -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true。


这里使用方式 1,修改一下上面用到的 RateLimitMain 方法:

@RateLimit(value = 666)
public class RateLimitMain {
    public static void main(String[] args) {
        System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        RateLimit rateLimit = RateLimitMain.class.getAnnotation(RateLimit.class);
        System.out.println(rateLimit.value());
    }
}

执行完毕之后,项目中多了一个目录:

其中$Proxy0是 @Retention 注解对应的动态代理类,而$Proxy1是我们的 @RateLimit 对应的动态代理类,当然如果有更多的注解,那么有可能生成 $ProxyN接着我们直接看 $Proxy1的源码:

public final class $Proxy1 extends Proxy implements RateLimit {
    private static Method m1;
    private static Method m2;
    private static Method m4;
    private static Method m0;
    private static Method m3;
    public $Proxy1(InvocationHandler var1) throws  {
        super(var1);
    }
    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }
    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
    public final Class annotationType() throws  {
        try {
            return (Class)super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
    public final int value() throws  {
        try {
            return (Integer)super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m4 = Class.forName("com.riemann.springbootdemo.annotation.RateLimit").getMethod("annotationType");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m3 = Class.forName("com.riemann.springbootdemo.annotation.RateLimit").getMethod("value");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

显然,$Proxy1实现了 RateLimit 接口,它在代码的最后部分使用了静态代码块实例化了成员方法的 Method 实例,在前面的代码对这些 Method 进行了缓存,在调用成员方法的时候都是直接委托到 InvocationHandler(AnnotationInvocationHandler) 实例完成调用。我们在分析 AnnotationInvocationHandler 的时候看到,它只用到了 Method 的名称从 Map 从匹配出成员方法的结果,因此调用过程并不是反射调用,反而是直接的调用,效率类似于通过 Key 从 Map 实例中获取 Value 一样,是十分高效的。

好了,这一篇显然解决了上一篇“注解是如何通过反射获取值的呢?”的疑问,下一篇来说一下“AOP 切面织入底层是如何实现的呢?”的这个疑问,敬请期待~



欢迎大家关注我的公众号【老周聊架构】,Java后端主流技术栈的原理、源码分析、架构以及各种互联网高并发、高性能、高可用的解决方案。


喜欢的话,点赞、再看、分享三连。


相关文章
|
5月前
|
Java 编译器 Spring
一文搞懂:什么是注解?
一文搞懂:什么是注解?
30 0
|
6月前
|
Java 编译器 API
【面试问题】注解的实现原理?
【1月更文挑战第27天】【面试问题】注解的实现原理?
|
安全 Java 大数据
一文搞懂什么是“注解”
一文搞懂什么是“注解”
295 0
一文搞懂什么是“注解”
|
安全 Java 编译器
JAVA注解与反射:看这篇文章就够了2
JAVA注解与反射:看这篇文章就够了
79 0
|
安全 Java 编译器
JAVA注解与反射:看这篇文章就够了1
JAVA注解与反射:看这篇文章就够了
142 0
|
SQL 监控 Java
魔法反射--java反射进阶(实战篇)
相信很多人在初学反射的时候也都会有这个想法(我就不相信就只有我一个人这么蠢!!) 大多数人不了解反射的原因并不是不了解, 而是不知道它到底能用来干什么 今天就来为大家分享一下反射的用法
85 0
|
XML 缓存 Java
Java注解怎么用
Java注解怎么用
227 0
|
XML 缓存 NoSQL
JAVA开发常用框架注解与作用
JAVA开发常用框架注解与作用
122 0
|
SQL 设计模式 XML
面试官:MyBatis 插件有什么用途?说说底层原理?我竟然不会。。
面试官:MyBatis 插件有什么用途?说说底层原理?我竟然不会。。
400 0
面试官:MyBatis 插件有什么用途?说说底层原理?我竟然不会。。