spring解析注解之AnnotationUtils
一、spring版本号
5.3.32-SNAPSHOT
二、关键类说明
以下所有类都在org.springframework.core.annotation包下。对每个类的总结说明都是自己的理解,如有异议可以去看源码中的注释0.0
2.1、AnnotationTypeMapping
注解映射类。 将一个注解的所有信息进行整理。没有继承或实现任何类。
主要关注的属性字段如下表:
字段 | 说明 |
---|---|
private final AnnotationTypeMapping source; | 源注解映射信息。源注解本身实例此属性为null |
private final AnnotationTypeMapping root; | 根注解映射信息。根注解此属性就是自身实例对象 |
private final int distance; | 当前注解的AnnotationTypeMapping实例到源注解的AnnotationTypeMapping实例的层级,源注解的AnnotationTypeMapping实例本身为0 |
private final Class<? extends Annotation> annotationType; | 注解类型的Class对象,泛型为任何继承自Annotation的类 |
private final List<Class<? extends Annotation>> metaTypes; | 自身annotationType及所有上级的注解AnnotationTypeMapping实例中annotationType属性的集合 |
private final Annotation annotation; | 注解代理类生成的实例对象。因为源注解在使用的时候用户一般会指定其中某些属性的值,所以此属性为null。而源注解中使用的其他注解已经固定不变,因此会将这些注解的代理的实例存在此字段。 |
private final AttributeMethods attributes; | 注解中所有的方法集合 |
2.2、AttributeMethods
注解中的方法集合类。将注解中所有方法进行整理。没有继承或实现任何类。
主要关注的属性字段如下表:
字段 | 说明 |
---|---|
private final Class<? extends Annotation> annotationType; | 注解的类型Class对象,泛型为任何继承自Annotation的类 |
private final Method[] attributeMethods; | 注解中方法的数组 |
private final boolean hasDefaultValueMethod; | 是否有默认值 |
private final boolean hasNestedAnnotation; | 是否有嵌套注解 |
2.3、AnnotationTypeMappings
注解映射类(AnnotationTypeMapping)的集合。没有继承或实现任何类。
主要关注的属性字段如下表:
字段 | 说明 |
---|---|
private final List<AnnotationTypeMapping> mappings; | 注解映射类(AnnotationTypeMapping)的集合 |
2.4、TypeMappedAnnotation
提供解析注解的相关方法,构造函数中包含JDK动态代理生成的注解的实例对象。
继承自AbstractMergedAnnotation<A extends Annotation>,实现了MergedAnnotation<A extends Annotation>
类图如下:
主要关注的属性字段如下表:
字段 | 说明 |
---|---|
private final AnnotationTypeMapping mapping; | 2.1、AnnotationTypeMapping |
private final Object rootAttributes; | 需要解析的注解的实例 |
private final ValueExtractor valueExtractor; | 从注解实例中获取属性值的方式 |
2.5、AnnotationAttributes
注解解析结果。
继承自LinkedHashMap<String, Object>。泛型指明KEY值为String类型。
类图如下:
2.6、小结
以上五个类中AnnotationTypeMapping、AttributeMethods、AnnotationTypeMappings存储注解的结构信息,所以spring在这三个类中加了缓存处理。
TypeMappedAnnotation类用来解析注解,所以构造函数中必须传入注解的实例。
AnnotationAttributes类使用Map来返回解析结果。
三、DEBUG源码分析
spring在解析注解的时候加入了自定义注解@AliasFor的处理,此篇文章不会对这种场景进行DEBUG跟踪。这里只对两种场景进行DEBUG代码跟踪分析。第一种是普通注解且没有嵌套注解的场景。第二种是有嵌套注解的场景。这里的嵌套注解指的是注解的某个方法的返回值是其他注解。
以下使用spring源码中的单元测试类org.springframework.core.annotation.AnnotationUtilsTests
3.1、场景1:无嵌套注解
先给出测试的注解
package org.springframework.core.testfixture.stereotype;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Copy of the standard {@code Component} annotation for testing purposes.
*
* @author Mark Fisher
* @since 2.5
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
* @return the suggested component name, if any (or empty String otherwise)
*/
String value() default "";
}
Component注解又被@Indexed注解注释(其他注解咱们可以忽略掉,因为spring解析的时候默认会忽略掉java.lang及org.springframework.lang包下的注解,后面DEBUG我们会看到)。
package org.springframework.core.testfixture.stereotype;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Copy of the standard {@code Indexed} annotation for testing purposes.
*
* @author Stephane Nicoll
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Indexed {
}
注解Component使用的地方是单元测试中的一个静态内部类(这里我把类中的内容都删除了,只留下了注解的信息)。
@Component("webController")
static class WebController {}
然后给出spring的单元测试方法。
@Test
void getAnnotationAttributesWithoutAttributeAliases() {
Component component = WebController.class.getAnnotation(Component.class);
assertThat(component).isNotNull();
AnnotationAttributes attributes = (AnnotationAttributes) getAnnotationAttributes(component);
assertThat(attributes).isNotNull();
assertThat(attributes.getString(VALUE)).as("value attribute: ").isEqualTo("webController");
assertThat(attributes.annotationType()).isEqualTo(Component.class);
}
启动单元测试。这里说明下,Java中的注解实例对象是通过JDK的动态代理创建的。
getAnnotationAttributes方法有许多的重载。咱们直接跳到目标方法查看。下图中的Adapt[] adaptations可以不用考虑。MergedAnnotation.from(annotatedElement, annotation)是接口MergedAnnotation中的方法,目的是返回TypeMappedAnnotation实例对象(2.4、TypeMappedAnnotation)。
在创建TypeMappedAnnotation对象前,需要先解析注解,生成AnnotationTypeMappings并缓存。
从缓存中获取Component注解的结构信息,如果没有拿到则创建并存入缓存。其中Cache是AnnotationTypeMappings的一个私有静态内部类。
没有缓存,新建Cache,然后通过Cache的get方法获取数据。咱们继续看Cache类中的get方法。get方法从缓存对象Cache中的mappings属性中获取数据,没有拿到则创建AnnotationTypeMappings(2.3、AnnotationTypeMappings)对象。
直接看AnnotationTypeMappings的构造方法。注意这里的构造方法没有传入注解的实例,只是传入了注解的Class对象(这里暂时不关心用户配置的注解信息,只关注注解本身的结构信息)。
构造方法中调用了addAllMappings(annotationType, visitedAnnotationTypes);方法。此方法中创建了一个队列。作用是循环解析Component注解需要的所有AnnotationTypeMapping(2.1、AnnotationTypeMapping),注解Component还被@Indexed注释,所以我们后面会看到创建了两个AnnotationTypeMapping实例。
addIfPossible方法中只是创建了AnnotationTypeMapping实例,我们直接看AnnotationTypeMapping的构造方法。
从入参annotationType可以看到先创建Component注解的AnnotationTypeMapping实例。
source为null。
distance为0(后面创建@Indexed注解的AnnotationTypeMapping实例时在此基础上+1)。
因为我们只关注Component注解的结构信息,因此annotation为空。
attributes是Component注解中的所有方法集合,可以看到只有一个value方法。
构造方法后面的操作是和嵌套注解及@AliasFor注解相关的,就不细看了。Component注解的AnnotationTypeMapping实例创建成功。返回addAllMappings方法。接下来需要创建Component注解中@Indexed注解的AnnotationTypeMapping实例。
addMetaAnnotationsToQueue方法入参中source参数就是刚创建的Component注解的AnnotationTypeMapping实例。通过AnnotationsScanner中的静态方法getDeclaredAnnotations获取Component注解被哪些注解注释,源码中会通过
AnnotationFilter PLAIN = packages("java.lang", "org.springframework.lang");
进行筛选,所以我们看到下面的metaAnnotations中有四个元素,前三个元素被置为null,最后一个元素就是通过动态代理生成的@Indexed注解的实例。接下来通过判断又会调用addIfPossible方法(前面通过此方法创建了Component注解的AnnotationTypeMapping实例)。
准备生成@Indexed注解的AnnotationTypeMapping实例。注意构造函数的入参,与Component注解不同的是,
1、入参source不再是null,而是刚才生成的Component注解的AnnotationTypeMapping实例,说明本次生成的AnnotationTypeMapping实例来源属于Component注解。(这样如果@Indexed注解再被其他注解注释,通过source属性就形成了类似链表式的数据结构)
2、入参ann也不再是null,而是动态代理生成的@Indexed注解的实例,因为在Component注解中@Indexed注解已经确定不会再有任何改变。
接下来生成@Indexed注解AnnotationTypeMapping的构造方法就不用细看了。
我们一直是在创建AnnotationTypeMappings实例。下图中AnnotationTypeMappings构造方法中创建Component注解相关AnnotationTypeMapping实例已经完成。后面为每个AnnotationTypeMapping实例执行其中的afterAllMappingsSet也是与别名相关的,这里不用看。
接下来创建TypeMappedAnnotation实例,TypeMappedAnnotation的构造方法中只获取AnnotationTypeMappings实例中第一个AnnotationTypeMapping实例(也就是Component注解的)。
参数annotation是通过动态代理生成的Component注解实例。
参数AnnotationUtils::invokeAnnotationMethod是指定如何调用注解中的每个方法的。
TypeMappedAnnotation实例创建成功后,我们就回到了最初的getAnnotationAttributes方法,直接看asMap方法。
先看下asMap方法的定义,如下:
/**
* Create a new {@link Map} instance of the given type that contains all the annotation
* attributes.
* <p>The {@link Adapt adaptations} may be used to change the way that values are added.
* @param factory a map factory
* @param adaptations the adaptations that should be applied to the annotation values
* @return a map containing the attributes and values
*/
<T extends Map<String, Object>> T asMap(Function<MergedAnnotation<?>, T> factory, Adapt... adaptations);
入参我们只关注Function<MergedAnnotation<?>, T> factory。这是java中的函数式编程,Function接口的定义如下(这里只列出了需要实现的apply方法)。可以看到Function的泛型要求apply方法入参为T类型,返回值为R类型。再看一下asMap方法的第一个入参:
mergedAnnotation -> new AnnotationAttributes(mergedAnnotation.getType(), true)
这行代码的意思是匿名实现了Function接口且T为TypeMappedAnnotation,R为AnnotationAttributes,意味着R apply(T t);方法的入参为TypeMappedAnnotation类型,返回值为AnnotationAttributes类型,同时AnnotationAttributes继承自LinkedHashMap<String, Object>(2.5、AnnotationAttributes)。
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
}
然后直接DEBUG跳到asMap方法的具体实现。第一行就是生成AnnotationAttributes对象实例,准备接收注解的解析结果。
AttributeMethods attributes = this.mapping.getAttributes();这一行代码就是从Component注解的AnnotationTypeMapping实例中拿到Component注解的所有方法,然后遍历获取每一个方法的返回值并存储到AnnotationAttributes实例中。可以看到KEY值为注解中的方法名,VALUE为处理后的结果。
直接看具体的getValue方法。
this.valueExtractor.extract(attribute, this.rootAttributes);就是获取注解中方法的具体值。
this.valueExtractor是我们在创建TypeMappedAnnotation对象实例时传入的获取注解中方法返回值的方式,这里使用的是JDK的动态代理获取的。
attribute参数是Component注解中的value()方法。
this.rootAttributes参数是通过JDK动态代理生成的Component注解实例。
后面源码还会对返回值类型进行处理,这里就不再展示了。
最后我们看一下解析结果,注意map的类型是AnnotationAttributes。
3.2、场景2:有嵌套注解
先给出测试的注解
/**
* Mock of {@code org.springframework.context.annotation.ComponentScan}.
*/
@Retention(RetentionPolicy.RUNTIME)
@interface ComponentScan {
Filter[] excludeFilters() default {};
}
嵌套注解如下:
@Retention(RetentionPolicy.RUNTIME)
@Target({})
@interface Filter {
String pattern();
}
注解使用如下:
@ComponentScan(excludeFilters = {@Filter(pattern = "*Foo"), @Filter(pattern = "*Bar")})
static class ComponentScanClass {
}
然后给出spring的单元测试方法。
@Test
void getAnnotationAttributesWithNestedAnnotations() {
ComponentScan componentScan = ComponentScanClass.class.getAnnotation(ComponentScan.class);
assertThat(componentScan).isNotNull();
AnnotationAttributes attributes = getAnnotationAttributes(ComponentScanClass.class, componentScan);
assertThat(attributes).isNotNull();
assertThat(attributes.annotationType()).isEqualTo(ComponentScan.class);
Filter[] filters = attributes.getAnnotationArray("excludeFilters", Filter.class);
assertThat(filters).isNotNull();
List<String> patterns = stream(filters).map(Filter::pattern).collect(toList());
assertThat(patterns).isEqualTo(asList("*Foo", "*Bar"));
}
这里我们DEBUG跟踪的时候只关注和3.1、场景1:无嵌套注解有差异的地方。
在创建ComponentScan注解中与方法相关的AttributeMethods对象实例时,参数hasNestedAnnotation为true,表示有嵌套注解。
还有就是在创建ComponentScan注解的AnnotationTypeMapping实例时,构造方法中会执行如下代码,如果发现嵌套注解,则生成嵌套注解的AnnotationTypeMapping实例。
在解析注解中的返回值的时候,返回值类型如果是注解类型或者注解数组。则会将注解值封装成TypeMappedAnnotation对象实例。如下图:
之后还会对返回值继续处理,最终返回的值还是注解的动态代理实例。
看一下解析的返回结果。
可以使用AnnotationAttributes提供的方法获取注解中的嵌套注解实例对象。
看一下getAnnotationArray(“excludeFilters”, Filter.class);方法,只是通过Map的get方法获取到数据,然后根据入参Class expectedType进行类型强转。
@SuppressWarnings("unchecked")
public <A extends Annotation> A[] getAnnotationArray(String attributeName, Class<A> annotationType) {
Object array = Array.newInstance(annotationType, 0);
return (A[]) getRequiredAttribute(attributeName, array.getClass());
}
@SuppressWarnings("unchecked")
private <T> T getRequiredAttribute(String attributeName, Class<T> expectedType) {
Assert.hasText(attributeName, "'attributeName' must not be null or empty");
Object value = get(attributeName);
assertAttributePresence(attributeName, value);
assertNotException(attributeName, value);
if (!expectedType.isInstance(value) && expectedType.isArray() &&
expectedType.getComponentType().isInstance(value)) {
Object array = Array.newInstance(expectedType.getComponentType(), 1);
Array.set(array, 0, value);
value = array;
}
assertAttributeType(attributeName, value, expectedType);
return (T) value;
}
可以看到内存中的对象实例都是同一份,只是进行了类型强转。
3.3、@AliasFor的处理
这里大概说一下spring对其自定义注解@AliasFor的处理。
@AliasFor注解会指定被标注的注解方法可以共用另一个方法的返回结果。同时spring将注解中的所有方法按顺序存储。在解析注解的时候会进行判断,被@AliasFor注解注释的方法是使用自己的方法返回值,还是使用@AliasFor注解指向的数据。
举一个例子:
定义注解
/**
* Mock of {@code org.springframework.web.bind.annotation.RequestMapping}.
*/
@Retention(RetentionPolicy.RUNTIME)
@interface WebMapping {
String name();
@AliasFor("path")
String[] value() default "";
@AliasFor(attribute = "value")
String[] path() default "";
RequestMethod[] method() default {};
}
使用注解
@WebMapping(value = "/test", name = "foo")
public void handleMappedWithValueAttribute() {
}
可以看到用户将WebMapping注解中value配置为“/test”。同时path方法被@AliasFor(attribute = “value”)指向value方法。下图是debug看到的结果,WebMapping注解解析后各方法的排序结果为:path下标为2,value的下标为3。所有可以看到各方法在获取值的时候实际应该使用的方法为[0,1,3,3]。意味着当解析value和path方法的时候都会使用value的方法返回结果。
四、总结
看spring解析注解的源码就像在看一份需求的实现。而且解析注解这份“需求”并不简单。
spring对注解的抽象,以及哪里该加缓存值得我们学习借鉴。
如果我们项目中依赖了spring的core包,解析注解的方法就不需要自己再实现了,使用现成的即可。