这篇博客的两个主题: spring的AnnotatedElementUtils 个人源码阅读方法论分享 为什么要分享AnnotatedElementUtils这个类呢,这个类看起来就是一个工具类,听起来很像apache的StringUtils,C…
这篇博客的两个主题:
spring的AnnotatedElementUtils
个人源码阅读方法论分享
为什么要分享AnnotatedElementUtils这个类呢,这个类看起来就是一个工具类,听起来很像apache的StringUtils,CollectionUtils。
原因是,它包含着spring对java注解的另类理解,和运用。
一、java的是怎样支撑注解的
1 | Class<TestAnnotation> clazz = TestAnnotation.class; |
}
`而虚方法和静态分派则是:```java
1 | // 实例不确定,方法也不确定。 |
}
1 | 上面提到了重载,而静态分派就是用以确定重载版本的,下面我要说的是覆写。覆写会导致不同实例的覆写版本,方法体不一样,所以虚拟机只能在运行期通过对象的实际类型来决定调用哪个版本的覆写方法。这被称为动态分派。 |
打锚点,协助思维跳跃 在读源码的时候,经常遇到你要跨越很多次方法调用的情况,人脑的栈是比较小的,所以我通过打锚点的方式,来协助大脑记忆调用栈。 打锚点是通过在关键代码上标注 todo 注释实现的。例如下图中的“// todo wanxm 1.15.1”。配合这个正则表达式: todo wanxm (1.15)\.?\d+.?( |$) 这里相当于列出了一条1.15.x的链,x可以是增量的,表示着某种你想要的先后顺序(比如方法调用顺序,逻辑点顺序等)。前缀1.15也是可变的,例如你在图中看到的这条1.15.x的链,其实是我在读一条1.x的链,读到1.15这个点上时,我发现它后面有挺多容,于是我在1.15这个点上使用1.15.0开了一条嵌套链。当我使用Idea的Ctrl + Shift + F 搜索时,使用“todo wanxm (1).?\d+.?( |$)”我就能看到那条1.x的链,使用“todo wanxm (1.15).?\d+.?( |$)”时,就能看到那条1.15.x的链。
1 | 使用idea的Ctrl + Alt + H,来跟踪类的初始化链。 |
/**
- 设AnnotationTypeMapping的某个实例为M,M所映射的注解为A。
- A中有5个属性(方法):H0,H1,H2,H3,H4。(H后面的数字表示方法的行文索引)问题出在:“如果所有属性都是默认值,则result = -1”(参看前文对MirrorSet的resolve方法的注释截图)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114*/
```
### 六、源码阅读方法论——步骤
开始之前先定目的 目的,在我们读源码的过程中,是非常重要的,其一:如果没有一个清晰明确的目的,你很可能被程序中纷繁的细节所包围,抓不住重点,搞不清楚自己要干什么,有了明确的目的,可以让你在深陷细节泥潭时跳脱出来,重新寻找支点。其二:要确定目的则你必需对你所要阅读的代码有一定的了解,这能促使你在阅读前,先去做一定的准备工作,从侧面先对代码有一个概念性的,笼统的认识。
构建依赖图 依赖图的构建方法有很多,你可以是从其他文章中看来的,也可以是自己找一个切入点,速读代码构建依赖图。
根据依赖图,自底向上,逐个理解类以及接口 理解类的时候,先以类的构造函数(或设值函数)为主,功能方法为辅理解类的表;后以表为支点,理解类。
### 七、带着方法论,探索Spring
到这里,今天的主角AnnotatedElementUtils就要登场了,它虽然名字叫做utils,但它可不是一个工具类那么简单,它蕴含着spring对注解这种语法的思考。
我是通过阅读《Spring Boot 编程思想》这本书了解到AnnotatedElementUtils的,书中没有详细展开介绍它,但是通过书中的描述,我知道了spring对注解的处理,是不同于java反射的语义的。我们就来读一读,看看有什么奥秘。我的阅读目的就是:spring怎样让注解实现属性覆写?
### 八、先展示一下AnnotatedElementUtils的作用
@TestA
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface TestRoot {
}
@TestB
public @interface TestA {
@AliasFor(value = "c1", annotation = TestC.class)
String bb() default "testA";
}
@TestC
public @interface TestB {
@AliasFor(value = "c2", annotation = TestC.class)
String cc() default "testB";
}
public @interface TestC {
@AliasFor(value = "c2")
String c1() default "testC";
@AliasFor(value = "c1")
String c2() default "testC";
}
```
### 九、从AnnotatedElementUtils开始构建依赖图
AnnotatedElementUtils这个类没有表,显然它只是某些其他类的代理,既然没有表,按照我们的方法论,它就没有什么太多可理解的了,我们读一读它的注释:
AnnotatedElementUtils(类)
查找注释通用方法。就是从AnnotatedElement上获取注释信息。
它和jdk提供的原生反省不一样。
它提供了两类查找,一类是get语义的查找,一类是find语义的查找
get语义的查找可查找到直接定义在AnnotatedElement上的,或继承来的注解。
find则比get要广
AnnotatedElement是类,则搜索这个类的超类,和接口
AnnotatedElement是方法,则搜索这个方法的桥接方法,以及超类和接口中的方法。(桥接方法:https://blog.csdn.net/alex_xfboy/article/details/86609055)
get和find都支持@Inherited
在组合注解中的元注解的属性复写功能被以下方法(及其重载方法)支持
getMergedAnnotationAttributes
getMergedAnnotation
getAllMergedAnnotations
getMergedRepeatableAnnotations
findMergedAnnotationAttributes
findMergedAnnotation
findAllMergedAnnotations
findMergedRepeatableAnnotations
从注释里我们知道,它有一堆get和find方法,find方法的语义看起来更接近我所提出的问题,所以我选择了findMergedAnnotationAttributes来作为切入点。迅速的阅读一下这个方方法,找出它当中依赖了些什么。
(下面给出依赖图)
### 十、从底层开始阅读
在这个依赖图中,最底层的类是:AttributeMethods,RepeatableContainers的实现类,AnnotationsScanner的实现类,AnnotationFilter,这几个底层类是比较简单的,所以今天我不讲他们,我讲AnnotationTypeMapping,按照正常的顺序,你应该是先去读它们的。
阅读注释 现在让我们聚焦到AnnotationTypeMapping这个类上,它的注释是这样写的:
以根注解为上下文,提供单个注解的映射信息。第一次读这句话我觉得没有几个中国人能理解,好在注释只是理解的手段之一。
阅读构造函数
metaTypes,annotationType,annotation,attributeMethods,aliasMappings,aliasedBy 在这里我就不赘述所有成员变量(也就是这个类的表)的阅读过程了,总之它们都是通过读AnnotationTypeMapping的构造函数而理解的,下面我直接贴图,展示了我如何对已经搞清楚的成员变量进行注释的。
mirrorSets,conventionMappings,annotationValueMappings,annotationValueSource 这几个则是依赖MirrorSet这个类的逻辑。MirrorSet是一个内部类,内部类和普通类的一个重要区别就是,当它被实例化的时候,所使用的数据不全来自构造函数的参数,还会来自其外部类的表,所以在阅读内部类的构造函数时,要先将它所使用的外部类的表理解了。 我面对内部类的策略是,尽量推迟阅读内部类的时间,也就是说,如果不是它阻碍了流程,那么就先将其搁置。(你掌握越多的外部类信息,则理解内部类时就越少会遇到卡壳的情况,避免在两个类之间反复切换的情况发生) 在不理会MirrorSets及其相关逻辑的情况下,我们已经疏通了上面那部分数据表的逻辑,根据那些信息,我可以比较容易的得出MirrorSets以及MirrorSet的表。 这两内部类就属于,表很简单,其功能更多由功能方法决定的类。我们去读一读它的各个功能方法(如果功能方法的参数你无法理解,有两种策略,1:使用类似阅读构造函数的方法;2:先不理他,等以后阅读其他代码时,发现调用到了这个功能方法,那时你带着相关参数的含义再来读这个功能方法)(我在这使用了第二种方法)。 这里贴出展现这两个内部类核心能力的代码注释: MirrorSets的: MirrorSet的:
读完MirrorSets的相关逻辑后,整个AnnotationTypeMapping的表的信息就有了。这里贴出它的全部表信息。
到这里,AnnotationTypeMapping这个类其实已经读的差不多了,总结一下,spring引入了如下层级属性概念:
别名属性 直接通过@AliaseFor关联起来的属性
镜像属性 由于直接或间接的@AliaseFor关系,使得某些属性实际上一定拥有相同的值,这些属性被称为镜像属性。
惯例属性 位于注解A中的和Root中同名的属性,被称为惯例属性,并且,同一注解中的惯例属性的镜像属性也是惯例属性。如,A中的H0和H1互为镜像属性,Root中的某个方法Hr和A中的H0名字相同,则Hr是Ho的惯例属性,Hr也是H1的惯例属性。
最低阶属性 A的最接近Root的有效属性。相当于,对A的某个属性来说,当低阶上存在它的镜像时,就取低阶的值,否则取它自己的值。由于低阶具有高优先级,所以我将它称作“最低阶属性”。
看了AnnotationTypeMapping的表,你的脑袋里是否已经有了它的概念了呢?
AnnotationTypeMapping提供了三个关键功能方法,分别是
getAliasMapping 用以获取root中的别名属性行文索引
getConventionMapping 用以获取root中的惯例属性的行文索引
getMappedAnnotationValue 获取最低阶属性的值
这三个方法就形成了spring获取注解属性的基础能力。
### 十一、回到开篇——spring是如何赋予注解覆写能力的
在spring中,注解之间具有多种关系,并且存在层级概念。使用者输入“别名关系”,spring则将这种关系深化,最终落到“惯例关系”与“最低阶关系”上,从而赋予低阶注解属性影响高阶注解属性的能力,实现低阶对高阶的覆写,就像子类对父类的覆写一样。并且,值得注意的是,spring并没有真的去修改高阶注解的属性值,而是通过类似指针的方式,将获取高阶注解属性值的操作指向它的低阶镜像,从而在外部看来,像是高阶属性被低阶属性覆写。
这种能力可以为我们带来什么优势?
以spring的@Service注解为例,它被@Component注解元标注,并且其value属性被标识了是@Component的value属性的别名。spring在为我们提供@Service注解的时候,并不需要专门去写一个注解处理器来将被@Service标注的类注册成Bean,spring只需要一个@Component的注解处理器就可以,因为它可以从任何被@Service标注的类上获取到@Component,并且获取到被覆写的value值。这是不是很像向上转型,很像多态?
对于广大的互联网开发人员来说,我们的基础工作栈之一就是spring,当我们在spring应用中开发时,何不使用spring已经搭建好的脚手架呢,当我们需要开发一些注解处理器的时候,完全可以使用spring封装好的AnnotatedElementUtils。
### 十二、题外话
大家有没有注意到MirrorSet的resolve方法有问题?}1
2
3
4
5-1表示的是它在某组镜像属性中没有找到有效属性,如果没有找到有效属性,那么某个高层注解的“最低阶属性”就不可能定位到这组镜像上来。
举个例子说明它会导致的问题:
```java
@AliasFor(value = "b1", annotation = TestB.class)
String a1() default "testA";
1 |
|
1 |
|
}
1 | // 这里你得到的实例b有两个key,b1和b2,值都是"testA" |
}
`但是当你将TestA修改成这样,使得a1和a2成为镜像属性时,得到的结果就比较奇怪了```java
1 |
|
}
1 | // 这里你得到的实例b有两个key,b1和b2,值都是"testB" |
}
注解TestB中的属性并没有被TestA中的属性覆盖,但TestA确实是TestB的低层级属性,它理应具有覆写上层属性的能力,当TestA中的属性没有形成镜像时,它确实表现出了这种能力,但当TestA中的属性形成镜像时,这种能力消失了(这个bug在spring-framework5.2.x版本下存在,将可能于5.2.3版本修复)。
比较幸运,我们发现了一个spring的bug。也从侧面证明了,我们的源码阅读方法论是有效的。给spring提一个PR,我们就能收到几个感谢。
### 十三、结语
AnnotatedElementUtils的能力其实并不是一个AnnotationTypeMapping可以概括的,还有其他一些类在整个逻辑中发挥重要作用,我会继续更新博客,慢慢将完整的AnnotatedElementUtils展现出来,而面对今天的AnnotationTypeMapping,你在看了表的注释后,有一个概括性的认识就可以了。
希望我的方法能对大家有所帮助,也期望大家和我分享你们的方法,让我们取长补短,最后能得出一套高效的方法论。
本文标题: 由AnnotatedElementUtils延伸的一些所
发布时间: 2019年09月16日 00:00
最后更新: 2025年12月30日 08:54
原始链接: https://haoxiang.eu.org/500e51ef/
版权声明: 本文著作权归作者所有,均采用CC BY-NC-SA 4.0许可协议,转载请注明出处!

