通过源码看 Java 注解本质

文章图片可能不太清晰,清晰版本请移步到:http://mp.weixin.qq.com/s?__biz=MzIzMjYzODEzMw==&mid=2247483658&idx=1&sn=0a6d6bd62e8967d096b09d1fc4decf59&chksm=e89092d0dfe71bc6625ddd63e5816ee9eb6908f98f1b0568264751ddd2412a70abe414a619a4#rd

说到注解,我们首先想到的可能是  Spring 的注解,或者自定义注解,其实Java本身也自定义了一些注解,Java 源码的注解是所有注解的基石,本文基于 Java 源码来看看这些基础注解,慢慢揭开注解的本质,看看注解到底是什么,以方便我们工作中使用好注解。

如果用一句话来描述注解的话,我可能会说注解是一种元数据,主要作用是用来描述业务的。

比如在工作中,我们常常定义一些日志切面注解,注解会定义在对外的接口上,来帮助我们自动的打印日志,这样我们就不需要在每个对外的接口实现里面每次都手动的打印日志了。

Java 元数据注解解析

Java基础注解,也叫做元数据注解,主要是用来描述一个注解是什么的,比如描述注解的生存阶段,注解的使用对象等等,通过下图目录我们可以看到元数据注解种类很多,但我们常用的一共 5 种,如下图中标红的:

通过源码看 Java 注解本质_第1张图片

接下来我们简单的介绍下这 5 种注解,以方便我们在工作中使用它们。

 

@Target

源码里面的解释:

通过源码看 Java 注解本质_第2张图片

 

可以简单理解成注解的使用对象是谁,目前使用对象的种类在 ElementType 类里面有定义,如下图( ElementType ):

通过源码看 Java 注解本质_第3张图片

 

ElementType 的枚举元素的名称 在 Java 里面都有相同命名的 Java 文件,我的理解可能是为了统一,毕竟当我们在 @Target 注解上面说明了使用范围时,编译器是需要去检查的,这其中是需要一个映射关系的,这样命名可能就是一种设计的约定。

@Target 注解我们是可以传数组,表示可以同时作用在多个对象上,源码如图:

通过源码看 Java 注解本质_第4张图片

 

@Retention

源码解释如下:

通过源码看 Java 注解本质_第5张图片

Retention 为我们提供了注解的保留策略,比如说我们想自己写个自定义注解,用来实现静态扫描的功能,那么这个注解的保留策略选择 CLASS 就好了,如果我们的自定义注解是为了实现动态的监控的话,那么保留策略应该选择 RUNTIME,三种策略的解释上图中都有。

 

@Inherited

如果自定义注解上加了这个元数据的话,如果自定义注解被用到了类上,一旦这个类被继承,子类是可以继承父类自定义注解的一切信息的,举了一个例子:

通过源码看 Java 注解本质_第6张图片

打印出的结果,显示的是父类 BaseTest 对 TestInheritedAnnotation 注解的赋值,说明注解的值已经被子类继承了,子类可以直接获取,如果  @Inherited 去掉的话,子类就不能够获取父类的注解信息了。

 

@Repeatable

注解一般都是不能重复打的,如果需要在方法或者类上重复的使用注解的话,就需要此元数据注解的帮助了,demo如下:

通过新建另外一个注解,注解内必须有个方法名称是 value 的方法,来达到注解可以重复的书写的目的。

 

注解的本质

注解到底是什么。我们使用 javap 解析下注解,我们会发现注解其实是个接口,天然的继承了 Annotation 接口,我们在注解里面定义的值其实都是抽象方法,截图如下:

 

注解本质是一个接口,接口是无法直接使用的,Java 通过默认生成一个子类的方式来使用注解,以方便储存和使用注解的值,我们通过启动 hsdb ,在 JVM 运行时 debug 发现,JVM 通过代理自动给注解生成一个实现类:

ps:

hsdb的启动命令是:

java -classpath  /Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/lib/sa-jdi.jar sun.jvm.hotspot.HSDB

 

我们工作中常常用的 Spring 的注解,常常会结合 Spring 的 AOP 特性,AOP 其实也是通过动态代理,来实现更多的功能,比如 Spring 的 @Transactional 注解,我们在 public 方法上写上这个注解,Spring 在初始化 bean 的时候,发现有这个注解,就会生成一个代理 bean ,运行过程主要是:在进入该方法前,初始化事务,然后进入方法,监听方法的运行状态,如果方法正常运行结束,则事务提交,检测到一切异常,则事务回滚,这里事务的提交和回滚都是通过 @Transactional 注解生成的切面代码来控制的。

 

注解如何存贮和解析的:

我们在前面的示例中使用过这样的代码:

new ChildTest().getClass()

.getAnnotation(TestInheritedAnnotation.class);

我们直接Class中通过 getAnnotation 可以直接得到注解,

说明 Class 中储存了注解的信息,我们打开源码看看是否是这样:

通过源码看 Java 注解本质_第7张图片

上图是 Class 类的继承关系。Class 直接实现了AnnotatedElement接口,我们关于注解的解析都定义在 AnnotatedElement 这个接口里面的,我们看看 AnnotatedElement 接口还被那些子类实现:

通过源码看 Java 注解本质_第8张图片

我们发现,所有可能会打上注解的地方,都继承了 AnnotatedElement 接口。

 

AnnotatedElement 常用的几个方法如下图:

通过源码看 Java 注解本质_第9张图片

getAnnotation和getAnnotationByType的区别主要在于

getAnnotationByType能够得到重复的注解,比如你在一个方法上面写了两个相同的注解,那么这个就叫做重复的注解,这时候应该使用getAnnotationByType。

我们通过 Class 文件的源码来看看是如何得到注解信息的,其余 Method , Field 的实现原理差不多。

通过源码看 Java 注解本质_第10张图片

Class 维护了 annotationData 的数据结构,维护的源码如下:(while true 的作用是保证 CAS 算法的成功,一旦 CAS 失败后,会重新 createAnnotation,直到更新成功为止 )

通过源码看 Java 注解本质_第11张图片

annotationData的数据结构为:AnnotatedElement 的所有行为,底层都是通过操作 annotationData 来实现的。

通过 while(true) 的死循环保证 JVM 实例化类-> 解析注解信息时,一定是能够成功的,annotationData 本身又是 class 的一个属性,是 volatile transient 属性的,保证了一旦更改,所有线程都能够读取到,通过双重手段保证了 JVM 在初始化注解信息时一定是安全可靠的。

更多的内容请扫描下面的二维码关注,方便交流,谢谢。

个人公众号主要研究 Java、领域驱动设计、中台建设等方面,

通过源码看 Java 注解本质_第12张图片

 

你可能感兴趣的:(Java,注解)