Spring当中对于Meta元注解的Merge流程

1.Spring当中原生的Bean导入的注解 Spring当中,最基础的注解是@Component注解,标注这个注解的类将会被Spring扫描并加入到BeanFactory当&

1.Spring当中原生的Bean导入的注解

Spring当中,最基础的注解是@Component注解,标注这个注解的类将会被Spring扫描并加入到BeanFactory当中。

对于Spring当中的另外的一些注解,包括@Service/@Controller/@Repository/Configuration等一系列注解,都是因为它们间接标注了@Component直接,所以也能被Spring扫描并加入到BeanFactory当中。

那么,你是否有想过,为什么标注@Configuration注解,为什么就能实现间接@Component注解的相关的信息?

// SearchApplication
@Configuration("app")
@FooAnnotation
@FooAnnotation2  
class SearchApplication {  
  
}

Spring当中的@Configuration注解的定义如下,定义当中确实有标注@Component注解。

@Target(ElementType.TYPE)  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
@Component  
public @interface Configuration {  
    @AliasFor(annotation = Component.class)  
    String value() default "";   
}

如果你尝试使用下面的代码,去获取@Component注解,你会发现无法获取到,获取到的是null

// component=null
Component component = SearchApplication.class.getAnnotation(Component.class);

但是Spring当中却可以获取到@Component注解的相关属性信息,你通过@Configuration去配置的beanName是可以生效的。

原因在于Spring当中自己实现了MergedAnnotationsMergedAnnotation的概念,专门用来处理这种间接注解(Meta元注解)的情况。

2.MergedAnnotation和MergedAnnotations

2.1 MergedAnnotation相关API的使用

对于MergedAnnotations和MergedAnnotation的使用方式参考如下:

// SearchApplication
@Configuration("app")
@FooAnnotation
@FooAnnotation2  
class SearchApplication {  
  
}

// usage
MergedAnnotations mergedAnnotations = MergedAnnotations.from(SearchApplication.class);  
MergedAnnotation<Component> mergedAnnotation = mergedAnnotations.get(Component.class);

可以基于MergedAnnotations.from相关的API,去构建一个MergedAnnotations,Spring当中的一个MergedAnnotations用来维护一个方法/字段/构造器上标注的所有的注解信息。

基于Spring提供的MergedAnnotation的相关的实现, 如果我们想要去获取到SearchApplication这个Bean的beanName,我们只需要使用以下的代码即可获取到。

// value=app
String value = mergedAnnotation.getString("value")

上面的代码当中,需要手动通过MergedAnnotation的相关的API去操作注解。

也许你会认为并不是那么直观,还是想要通过原生的@Component注解去调用相关属性方法去进行获取,因此Spring还提供了synthesize方法直接拿到Spring合成的注解@Component,通过返回的@Component注解再尝试去获取value属性,依旧可以正常到。

final Component synthesizedComponent = mergedAnnotation.synthesize();  
// value=app  
String value = synthesizedComponent.value();

我们以下面的SearchApplication类的注解结构进行说明MergedAnnotationMergedAnnotation是以什么样的方式存在的。

                                           |---@Annotation1
                                           |
                 |-----@FooAnnotation----------@Annotation2
                 |                   
                 |
SearchApplication------@Configuration----------@Component
                 |
                 |
                 |-----@FooAnnotation2---------@Annotation3
                                           |
                                           |---@Annotation4

对于@FooAnnotation@Configuration@FooAnnotation2三个注解,分别形成了一个注解的继承树。

我们以@Configuration注解为例,它将会以@Configuration注解作为root节点,向下级节点使用BFS广度优先遍历的方式进行遍历,形成一棵多叉树的结构,对于树当中一个节点将会使用AnnotationTypeMapping进行描述,对于这棵继承树则将会使用AnnotationTypeMappings进行描述。并且对于AnnotationTypeMappings当中的每个AnnotationTypeMapping节点,都会持有父节点的引用,也会持有root节点的引用。

我们通过下面这样的代码就可以自动构建出来这样的注解的继承关系。

MergedAnnotations mergedAnnotations = MergedAnnotations.from(SearchApplication.class);  

对于SearchApplication类上的@FooAnnotation@Configuration@FooAnnotation2三个注解都会分别构建出来一棵AnnotationTypeMappings继承树。

对于之前我们提到的,通过获取@Component注解,可以获取到@Service注解当中的配置的beanName,最终就是通过AnnotationTypeMapping形成的关系去解析得到的。

2.2 通过@AliasFor注解形成alias别名链条aliasedBy

Spring当中提供了@AliasFor注解去提供属性名之间的映射关系,下面是Spring当中的@Service注解的实现:

@Component  
public @interface Service {  
    @AliasFor(annotation = Component.class)  
    String value() default "";  
}

@Service注解的value属性,通过@AliasFor注解去指向了@Component注解的value属性。

@AliasFor注解有两个属性,通过这两个属性组合最终确定指向的目标注解的目标属性。(不允许指向属性自身)

  • 一个是annotation代表指向的目标注解的类,默认不填是代表指向当前注解;
  • 一个是attribute代表指向的目标注解的属性名称,不填代表是指向同名的注解。

通过@Service标注@Component注解,并且在属性当中通过@AliasFor注解,将当前@Service注解value属性去指向@Component注解的value属性,最终@Service注解的value属性,将会被映射到@Component注解的value属性当中去。

@Component.value   -->  @Service.value

@Service注解的内部,会维护一个aliasedByMap,结构是Map<Method, List<Method>>,Key是指向的注解属性,比如这里的@Component.value,Value是当前注解的所有当中,指向该属性的属性列表,因为存在有一个注解当中多个属性指向同一个注解。

@Component  
public @interface Service {  
    @AliasFor(annotation = Component.class)  
    String value() default "";  
    
    @AliasFor(attribute = "value", annotation = Component.class)  
    String name() default "";  
}

比如如上这样的情况的@Service注解,它的aliasedBy维护的情况就是如下这样的结构

@Component.value--->@Service.value
                |
                --->@Service.name

如果此时还存在有一个@MyService的注解,定义如下:

@Service  
public @interface MyService {  
    @AliasFor(annotation = Component.class)  
    String value() default "";  
    
    @AliasFor(attribute = "value", annotation = Component.class)  
    String name() default "";  
}

那么它的aliasedBy的结构如下

@Component.value--->@MyService.value
                |
                --->@MyService.name

当在一个自定义的业务Service当中去标注@MyService注解时:

@MyService("foo")
class FooService {  
  
}

当你尝试使用如下的代码尝试去获取@Component注解的value属性时:

// usage
MergedAnnotations mergedAnnotations = MergedAnnotations.from(FooService.class);  
MergedAnnotation<Component> mergedAnnotation = mergedAnnotations.get(Component.class);
String value = mergedAnnotation.getString("value");

MergedAnnotations会首先找到@Component注解的所在位置AnnotationTypeMapping,接着从继承树关系上去找到@Service@MyService对应的AnnotationTypeMapping,接着根据@Component.value@Service@MyService注解对应的aliasedBy当中去获取对应的alias关系,最终获取到的映射关系是如下的这个列表,都是候选的@Component.value属性的数据来源。

// @Component.value属性本身
@Component.value

// @Service注解当中存在的指向@Component.value的属性
@Service.value
@Service.name

// @MyService注解当中存在的指向@Component.value的属性
@MyService.value
@MyService.name

我们可以看到,@Component.value存在有多个别名属性,存在有这么多个属性值可能作为@Component.value的数据来源,最终会选取哪个作为最终的数据呢?

  • 越接近root节点的属性值优先级越高,上级节点的属性值会覆盖下级节点的属性值,因此会候选使用@MyService.value@MyService.name这两个属性作为@Component.value属性的来源。
  • 如果出现了同级别的多个属性可以作为候选属性(@MyService.value@MyService.name),那么选择规则如下:
    • 对于Spring当中会判断这两个属性值当中,哪个和默认值defaultValue不一致则会选择选择这个属性值。
    • 如果两个属性值都是默认值,那么按照属性名进行字典序排序进行选择前面的。
    • 如果两个属性值都不是默认值,如果这两个属性值一致那么选哪个都无所谓,如果两个属性值不一样,那么会抛出AnnotationConfigurationException异常。

2.3 aliasMapping和conventionMapping

  • aliasMapping,主要用于快速检索当root节点当中存在有对应的Meta注解的@AliasFor指向的别名映射的情况,比如上面提到的@Component.value属性,在根级别注解@MyService注解当中就能找到对应的属性@MyService.value(假设@MyService.value是最优的)。
  • conventionMapping,主要针对root节点当中存在有同名的属性时,支持进行快速映射,比如针对@Service.name这个属性,不存在有@AliasFor别名映射,此时可以快速根据name属性去root节点上去找到@MyService.name属性去作为@Service.name的属性(需要注意的是:value属性作为Java注解的默认属性,不支持conventionMapping映射,只能支持aliasMapping映射)。

2.4 Meta注解的父节点的属性配置解析

Comment