前言
众所周知,spring 从 2.5 版本以后开始支持使用注解代替繁琐的 xml 配置,到了 springboot 更是全面拥抱了注解式配置。平时在使用的时候,点开一些常见的等注解,会发现往往在一个注解上总会出现一些其他的注解,比如 @Service:
1 |
|
大部分情况下,我们可以将 @Service 注解等同于 @Component 注解使用,则是因为 spring 基于其 JDK 对元注解的机制进行了扩展。
在 java 中,元注解是指可以注解在其他注解上的注解,spring 中通过对这个机制进行了扩展,实现了一些原生 JDK 不支持的功能,比如允许在注解中让两个属性互为别名,或者将一个带有元注解的子注解直接作为元注解看待,或者在这个基础上,通过 @AliasFor 或者同名策略让子注解的值覆盖元注解的值。
本文将基于 spring 源码 5.2.x 分支,解析 spring 如何实现这套功能的。
这是系列的第三篇文章,将详细介绍 Spring 是如何在经过搜索与属性映射后,将处理后的注解合成为合并注解的。
相关文章:
一、合并注解
我们在前文了解用于搜索注解的合并注解聚合 MergedAnnotations与用于完成注解属性映射的 AnnotationTypeMappings 和AnnotationTypeMapping,现在我们需要知道在 MergedAnnotations 这个容器中,AnnotationTypeMappings 和AnnotationTypeMapping 是如何转为一个我们所需要的合并注解 MergedAnnotation 的。
与前文一样,我们以 AnnotatedElementUtils.findMergedAnnotations 方法作为入口:
1 | public static <A extends Annotation> A findMergedAnnotation(AnnotatedElement element, Class<A> annotationType) { |
我们在上文顺着 MergedAnnotations.get 一路找到 TypeMappedAnnotations.MergedAnnotationFinder 的 process 方法,在这里我们目睹了一个普通的注解的元注解被解析为 AnnotationTypeMappings 与 AnnotationTypeMapping 的过程:
1 | private MergedAnnotation<A> process( |
其他部分在上文已经详细的分析了,因此此处我们仅需关注 TypeMappedAnnotation.createIfPossible 这一个方法:
1 | static <A extends Annotation> TypeMappedAnnotation<A> createIfPossible( |
该方法是 AnnotationTypeMapping 转为 MergedAnnotation 的关键。
1、合并注解的创建
TypeMappedAnnotation 是 MergedAnnotation 一个通用实现,在大部分情况下,我们所说的合并注解其实指的就是这个类。
通过它的构造方法我们得以了解其创建过程:
1 | private TypeMappedAnnotation(AnnotationTypeMapping mapping, ClassLoader classLoader, |
可以看得出,TypeMappedAnnotation 基本可以认为是 AnnotationTypeMapping 的包装类,它以一个 AnnotationTypeMapping 实例作为数据源,从而提供一些关于映射后的属性的相关功能。
它本身并没有很多特殊的逻辑,我们仅需要关注通过它合成注解的代理对象,以及后续调用代理对象时,是如何从映射过的属性获取值的。
2、合并注解的合成
回到 AnnotatedElementUtils.findMergedAnnotations,我们可以看到,在通过 MergedAnnotations 获得了一个 MergedAnnotation 对象——实际上是 TypeMappedAnnotation 对象——之后,又调用了 MergedAnnotation.synthesize 方法,将 MergedAnnotation 转成了一个调用方指定类型的注解对象。
该方法先调用了 AbstractMergedAnnotation 的 synthesize 方法:
1 | public Optional<A> synthesize(Predicate<? super MergedAnnotation<A>> condition) |
随后再调用了实现类 TypeMappedAnnotation 的 synthesize 方法:
1 | public A synthesize() { |
继续点开 createSynthesized :
1 | protected A createSynthesized() { |
而 SynthesizedMergedAnnotationInvocationHandler 是一个用于 JDK 动态代理的 InvocationHandler,我们不需要完全站看,仅需看看它的构造函数与 InvocationHandler.invoke 就能明白它的运作机制了:
1 | final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> implements InvocationHandler { |
至此,合并注解的合成机制已经很明确了:
- 先通过
AnnotationTypeMapping创建TypeMappedAnnotation; - 通过
TypeMappedAnnotation.synthesize创建一个对应的类型的注解对象; - 如果该
TypeMappedAnnotation无需合成,则直接返回对应的原始注解对象,否则返回基于该合并注解生成的JDK动态代理对象;
二、合并注解的取值
承接上文,当我们使用 MergedAnnotation.synthesize 方法时,我们可能会得到两种对象:
- 第一种就是原始的注解对象,这个没什么好说的;
- 第二种就是通过
SynthesizedMergedAnnotationInvocationHandler生成的注解代理对象;
而通过注解代理对象取值时,这些方法会被代理到 SynthesizedMergedAnnotationInvocationHandler 中存放的 MergedAnnotation 对象上,从而让这个代理对象通过原始注解的属性,获得与原始注解不一样的属性值。
1、方法代理
当我们调用代理对象的属性值时,它会在 SynthesizedMergedAnnotationInvocationHandler 中,通过 invoke 代理到对应的方法上:
1 |
|
2、注解属性的获取
这里我们代理对象是如何获取注解属性值的:
1 | private Object getAttributeValue(Method method) { |
这里的 MergedAnnotation.getValue 最终在经过多次跳转后,调到 TypeMappedAnnotation.getAttributeValue 上:
1 | protected <T> T getAttributeValue(String attributeName, Class<T> type) { |
而这边的 getValue 方法就是真正要获取属性值的地方。
1 | private Object getValue(int attributeIndex, boolean useConventionMapping, boolean forMirrorResolution) { |
这一步有点复杂,主要是根据不同的情况,通过 AnnotationTypeMapping 中的几个属性映射数组,包括 aliasMappings、conventionMappings,annotationValueMappings 与 annotationValueSource 来确定最终用于取值的 AnnotationTypeMapping 对象与调用的方法在 AttributeMethods 中的下标:
- 如果要合并属性值,则:
- 若该属性被
root中的同名属性覆盖,即aliasMappings数组对应下标不为-1,则记录该root属性下标; - 若该属性不被
root中同名属性覆盖,则确定是否被子注解中的同名属性覆盖,即conventionMappings数组对应下标不为-1,则记录该覆盖属性下标;
- 若该属性被
- 若允许别名,则在上述基础上,继续进行处理:
- 若当前注解已经是根注解,则从
resolvedRootMirrors上,获得该属性在同一注解下关联的别名属性中,唯一确定的有效属性的下标; - 若当前注解不是根注解,则从
resolvedRootMirrors上,获得该属性在同一注解下关联的别名属性中,唯一确定的有效属性的下标;
- 若当前注解已经是根注解,则从
- 若当前注解是根注解,则使用从根注解对象
rootAttributes上根据属性下标获取对应方法,然后通过反射调用获取属性值; - 若当前注解不是根注解,则:
- 若不支持属性覆盖以及别名,则直接和获取该注解属性下标对应方法,并通过反射调用获取属性值;
- 若支持属性覆盖以及别名,则通过属性下标从
annotationValueSource找到对应的注解对象,再通过annotationValueMappings找到要在该注解中调用的属性下标,然后在通过属性下标找到对应的属性方法后,通过反射调用获取属性值;
至此,获取属性值的方法流程也走完了。
总结
在这一章,我们了解了当通过 MergedAnnotations 获得注解并解析得到 AnnotationTypeMapping 后,AnnotationTypeMapping 是如何再转为我们所需的 MergedAnnotation ,以及在此之后,MergedAnnotation 又是如何生成我们最终所需要的代理注解的。
简而言之,当解析注解的元注解获得所需的 AnnotationTypeMapping 后,MergedAnnotation 会判断 AnnotationTypeMapping 是否发生过属性映射,如果没有则返回该映射对象对应的原始注解,否则就通过 SynthesizedMergedAnnotationInvocationHandler 生成一个对应类型的 JDK 动态代理对象。
当我们通过代理对象去调用注解的方法,获取注解的属性的时候,SynthesizedMergedAnnotationInvocationHandler 会把方法代理到对应的内部方法中,而获取属性时,还会通过 MergedAnnotation.getValue ,最终绕到 AnnotationTypeMapping 中获取被映射后的属性值。
题外话
至此,关于 Spring 注解机制的三篇文章已经全部写完了,在了解了它是实现原理的同时,笔者也深刻的认识到了其功能的强大,尤其是 MergeAnnotation 真切的弥补了 java 注解不支持继承所带来的的一些痛点,这点笔者在自己的项目尝试了全面拥抱 spring 的增强注解后深有感触。
出于巩固知识,也出于为想要在非 Spring 环境下享受这个功能的同学考虑,作者决定搞一套类似的开源项目,目前也已经有了一些成果:
- 笔者尝试为常用的开源工具类库
hutool提了一个 PR ,也有幸被作者大佬采纳了,有需要的同学可以在Hutool-5.8.5及以上版本使用该功能; - 由于考虑有些项目并没有引入
Hutool,以及跟作者沟通后觉得这个特性并不适合搬到即将发布的Hutool-6.x,因此笔者单独将作为一个注解工具类库开源了,有兴趣的同学可以了解一下powerful-annotation;