1.概述

转载:JDK中注解的底层实现

前提

用Java快三年了,注解算是一个常用的类型,特别是在一些框架里面会大量使用注解做组件标识、配置或者策略。但是一直没有深入去探究JDK中的注解到底是什么,底层是怎么实现了?于是参考了一些资料,做了一次稍微详细的分析。

JDK的注解描述

参考JavaSE-8里面的JLS-9.6对注解的描述如下:


注解的声明如下:

SHELL
{InterfaceModifier} @ interface Identifier AnnotationTypeBody

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

其中:

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

既然所有注解类型的父接口都是java.lang.annotation.Annotation,那么我们可以看一下Annotation接口的文档:

public interface Annotation

The common interface extended by all annotation types. Note that an interface that manually extends this one does not define an annotation type. Also note that this interface does not itself define an annotation type. More information about annotation types can be found in section 9.6 of The Java™ Language Specification. The AnnotatedElement interface discusses compatibility concerns when evolving an annotation type from being non-repeatable to being repeatable.

JavaSE-8中的文档对Annotation的描述和JLS-9.6中差不多,不过最后指明了可重复注解的兼容性考虑的问题,可重复注解在JDK1.8中由元注解@Repeatable实现。下面基于JDK8的最后一个版本java version 1.8.0_181探究一下注解在JDK中的底层实现。

注解实现探究

我们先定义一个十分简单的Counter注解如下:

package club.throwable.annotation;import java.lang.annotation.*;@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.TYPE)
public @interface Counter {int count() default 0;
}

我们先从直接使用@Counter注解,从直观上观察@Counter实例的类型:

@Counter(count = 1)
public class Main {public static void main(String[] args) throws Exception{Counter counter = Main.class.getAnnotation(Counter.class);System.out.println(counter.count());}
}


@Counter实例从Debug过程中观察发现是JDK的一个代理类(并且InvocationHandler的实例是sun.reflect.annotation.AnnotationInvocationHandler,它是一个修饰符为default的sun包内可用的类),为了验证这一点我们使用JDK的反编译命令查看@Counter的字节码:

javap -c -v ...club\throwable\annotation\Counter.class

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

Classfile /D:/Projects/rxjava-seed/target/classes/club/throwable/annotation/Counter.classLast modified 2018-10-6; size 487 bytesMD5 checksum 83cee23f426e5b51a096281068d8b555Compiled from "Counter.java"
public interface club.throwable.annotation.Counter extends java.lang.annotation.Annotationminor version: 0major version: 52flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
Constant pool:#1 = Class              #19            // club/throwable/annotation/Counter#2 = Class              #20            // java/lang/Object#3 = Class              #21            // java/lang/annotation/Annotation#4 = Utf8               count#5 = Utf8               ()I#6 = Utf8               AnnotationDefault#7 = Integer            0#8 = Utf8               SourceFile#9 = Utf8               Counter.java#10 = Utf8               RuntimeVisibleAnnotations#11 = Utf8               Ljava/lang/annotation/Retention;#12 = Utf8               value#13 = Utf8               Ljava/lang/annotation/RetentionPolicy;#14 = Utf8               RUNTIME#15 = Utf8               Ljava/lang/annotation/Documented;#16 = Utf8               Ljava/lang/annotation/Target;#17 = Utf8               Ljava/lang/annotation/ElementType;#18 = Utf8               TYPE#19 = Utf8               club/throwable/annotation/Counter#20 = Utf8               java/lang/Object#21 = Utf8               java/lang/annotation/Annotation
{public abstract int count();descriptor: ()Iflags: ACC_PUBLIC, ACC_ABSTRACTAnnotationDefault:default_value: I#7}
SourceFile: "Counter.java"
RuntimeVisibleAnnotations:0: #11(#12=e#13.#14)1: #15()2: #16(#12=[e#17.#18])

如果熟悉字节码,从直观上可以得到下面的信息:

注解是一个接口,它继承自java.lang.annotation.Annotation父接口。
@Counter对应的接口接口除了继承了java.lang.annotation.Annotation中的抽象方法,自身定义了一个抽象方法public abstract int count();

既然注解最后转化为一个接口,注解中定义的注解成员属性会转化为抽象方法,那么最后这些注解成员属性怎么进行赋值的呢?答案就是:为注解对应的接口生成一个实现该接口的动态代理类。直接点说就是:Java通过动态代理的方式生成了一个实现了”注解对应接口”的实例,该代理类实例实现了”注解成员属性对应的方法”,这个步骤类似于”注解成员属性”的赋值过程,这样子就可以在程序运行的时候通过反射获取到注解的成员属性(这里注解必须是运行时可见的,也就是使用了@Retention(RetentionPolicy.RUNTIME),另外需要理解JDK原生动态代理和反射相关内容)。

注解对应的动态代理类实例

上面一些已经指出了,注解的最底层实现就是一个JDK的动态代理类,而这个动态代理类的生成过程我们完全可以通过Debug跟踪,这里列举一下笔者跟踪整个过程的流水账:

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

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

3、Class<?>#createAnnotationData(int classRedefinedCount),构建注解的数据。

4、AnnotationParser#parseAnnotations(byte[] var0, ConstantPool var1, Class<?> var2),这里已经是sun包下的类,无法看到源码,这个方法用于解析注解,这一步使用到字节码中常量池的索引解析,常量解析完毕会生成一个成员属性键值对作为下一个环节的入参,常量池的解析可以看AnnotationParser#parseMemberValue方法。

5、AnnotationParser#annotationForMap(final Class<? extends Annotation> var0, final Map<String, Object> var1),同样是sun.reflect.annotation.AnnotationParser中的方法,用于生成注解的动态代理类。

注意第5步,贴出它的源码:

JAVA
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()方法:

JAVA
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;}}}}
//忽略其他方法

这里需要重点注意一下的是:AnnotationInvocationHandler的成员变量Map<String, Object> memberValues存放着注解的成员属性的名称和值的映射,注解成员属性的名称实际上就对应着接口中抽象方法的名称,例如上面我们定义的@Counter注解生成代理类后,它的AnnotationInvocationHandler实例中的memberValues属性存放着键值对count=1

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

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

2、通过-D参数指定,其实跟1差不多,参数是:-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

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

JAVA
public static void main(String[] args) throws Exception {System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");Counter counter = Main.class.getAnnotation(Counter.class);System.out.println(counter.count());
}

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

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

JAVA
public final class $Proxy1 extends Proxy implements Counter {private static Method m1;private static Method m2;private static Method m3;private static Method m4;private static Method m0;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 int count() throws  {try {return (Integer)super.h.invoke(this, m3, (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);}}static {try {m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));m2 = Class.forName("java.lang.Object").getMethod("toString");m3 = Class.forName("club.throwable.annotation.Counter").getMethod("count");m4 = Class.forName("club.throwable.annotation.Counter").getMethod("annotationType");m0 = Class.forName("java.lang.Object").getMethod("hashCode");} catch (NoSuchMethodException var2) {throw new NoSuchMethodError(var2.getMessage());} catch (ClassNotFoundException var3) {throw new NoClassDefFoundError(var3.getMessage());}}
}

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

小结

既然知道了注解的底层原理,我们可以编写一个”注解接口”和InvocationHandler实现来简单模拟整个过程。先定义一个接口:

JAVA
public interface CounterAnnotation extends Annotation {int count();
}
InvocationHandler的简单实现:JAVA
public class CounterAnnotationInvocationHandler implements InvocationHandler {private final Map<String, Object> memberValues;private final Class<? extends Annotation> clazz;public CounterAnnotationInvocationHandler(Map<String, Object> memberValues, Class<? extends Annotation> clazz) {this.memberValues = memberValues;this.clazz = clazz;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {String methodName = method.getName();Object value;switch (methodName) {case "toString":value = super.toString();break;case "hashCode":value = super.hashCode();break;case "equals":value = super.equals(args[0]);break;case "annotationType":value = clazz;break;default:value = memberValues.get(methodName);}return value;}
}

编写一个main方法:

JAVA
public class CounterAnnotationMain {public static void main(String[] args) throws Exception{//这里模拟了注解成员属性从常量池解析的过程Map<String,Object> values = new HashMap<>(8);values.put("count", 1);//生成代理类CounterAnnotation proxy = (CounterAnnotation)Proxy.newProxyInstance(CounterAnnotationMain.class.getClassLoader(),new Class[]{CounterAnnotation.class},new CounterAnnotationInvocationHandler(values, CounterAnnotation.class));System.out.println(proxy.count());}
}
//运行后控制台输出:1

【java】JDK中注解的底层实现相关推荐

  1. 观察者模式--java jdk中提供的支持

    一.简介 观察者设计模式有如下四个角色 抽象主题角色:把所有对观察者对象的引用保存在一个集合中,每个抽象主题角色都可以有任意数量的观察者.抽象主题提供一个接口,可以增加和删除观察者角色.一般用一个抽象 ...

  2. Java JDK中几个经典的版本

    Java JDK中几个经典的版本 1. 1995年05月23日 JDK 1.0 版发布,于1996年JDK正式提供下载,标志着Java的诞生. 2. 1998年12月04日 JDK 1.2 版发布,而 ...

  3. 【java】java 理解JDK中UUID的底层实现

    1.概述 先看这个:可笑,你竟然不知道 Java 如何生成 UUID 转载:https://www.cnblogs.com/throwable/p/14343086.html 2.前提 UUID是Un ...

  4. 冷饭新炒:理解JDK中UUID的底层实现

    前提 UUID是Universally Unique IDentifier的缩写,翻译为通用唯一标识符或者全局唯一标识符.对于UUID的描述,下面摘录一下规范文件A Universally Uniqu ...

  5. JDK中Timer定时器底层到底是怎么实现的?

    关于定时器在JDK中提供了两种方式,一种是 Timer(同步调度) ,另一种是ScheduledThreadPoolExecutor(异步调度). 更高级的任务调度开源框架就比较多了,但最核心的定时器 ...

  6. Java jdk中的部分工具javac javaw javaws javadoc javah javap jar jdb jps的作用

    一.JDK 工具和实用程序 JDK提供的标准工具和程序 基本工具:javac, java, javadoc, apt, appletviewer, jar, jdb, javah, javap, ex ...

  7. java jdk中的归并排序实现

    在Arrays.java中的sort中 public static void sort(Object[] a, int fromIndex, int toIndex) {if (LegacyMerge ...

  8. java之自定义注解的完整使用

    小坏java自定义注解的完整使用 一.何为java注解之道 1.java 注解的理解之道 2.java 注解的使用示例之道 3.Java 如何自定义注解之道 4.java 元注解之道 5.java 如 ...

  9. 设计模式:单例模式在JDK中的应用

    在java jdk中, java.lang.Runtime就是经典的单例模式. 以下截取Runtime部分代码 package java.lang;import java.io.*; import j ...

最新文章

  1. accp8.0html作业,Accp8.0HTML标签
  2. js如何去除多个cookie?
  3. Cisco之ARP配置
  4. detime php_php试题及答案
  5. 2021年度脑机接口重大事件和进展汇总
  6. strstr c语言字符串,C字符串处理strstr, strindex
  7. java protobuf 例子_用Maven实现一个protobuf的Java例子
  8. redis的安装和使用【2】redis的java操作
  9. 汇编工具安装三:已经配置好的汇编开发工具!
  10. Java最佳实践–字符串性能和精确字符串匹配
  11. Handler、Message的简单使用
  12. MySQL:Innodb page clean 线程 (二) :解析
  13. 树莓派学习之路-GPIO Zero
  14. [Andriod设计模式之旅]——Builder模式
  15. 人工智能与机器学习大牛们的blog
  16. C语言:输入四个数a,b,c,d,将这四个数由小到大排序
  17. C#使用Minidump导出内存快照Minidumper
  18. HDMI接口 HDCP
  19. 向下舍入是什么意思_舍入是什么意思
  20. 最详细的Log4j使用教程

热门文章

  1. 外卖小哥高考623分!查分后淡定送外卖
  2. 魅族预热魅族18系列:没有中杯 大杯起步
  3. 特斯拉Autopilot系统被评为中国最佳驾驶辅助系统
  4. 曝iPhone 12 Pro耗电异常,没开启任何应用电量也急剧下降
  5. 《财富》评2020年最受赞赏中国公司:华为第一 小米第三
  6. 三星首款5nm A78旗舰芯Exynos 1080即将亮相
  7. 华为官方开启Mate 40系列预热:即将推出敬请期待
  8. 数百辆共享单车被丢垃圾场!官方回应...
  9. 瑞幸咖啡退市成定局:董事长被要求辞职,新店却仍在扩张
  10. 疫情挡不住上市步伐:视频模拟敲锣 A股云上市了解一下