封面来源:碧蓝航线 湮烬尘墟 活动 CG
本文涉及的代码:
1. @AliasFor 注解
1.1 @AliasFor 的简介
Java 中的注解是在 JDK 1.5 引入的,注解的出现极大地简化了程序员对 Java 程序的描述,但其中也存在一些缺陷,比如注解彼此之间没有继承关系、无法将一个注解上的属性值传递给另一个注解,为了解决这些问题,Spring 引入了 @AliasFor
注解。
顾名思义,@AliasFor
注解是用于取别名的,能够将一个注解上的属性值传递给另一个注解,或者让同一个注解中的两个属性互为别名。注意, 这些特性不是 Java 原生支持的, 因此需要额外使用 AnnotationUtils
或 AnnotatedElementUtils
工具类来解析。
@AliasFor
适用于三种场景:
- 注解中的显式别名
- 元注解属性的显式别名
- 注解中的隐式别名
1.2 注解中的显式别名
这指的是:在同一个注解中的不同属性上使用 @AliasFor
注解,表示它们是彼此可互换的别名。
使用要求
- 组成别名的每个属性都应该被
@AliasFor
注解标记,并且 @AliasFor
中的 attribute
或 value
属性必须引用互为别名的属性中的另一个属性。从 Spring 5.2.1 开始,可以只标记别名对中的一个属性,但为了更好的兼容性,还是建议标记别名对中的两个属性;
- 互为别名的属性必须具有相同的返回类型;
- 互为别名的属性必须声明默认值,并且声明相同的默认值;
- 不使用
@AliasFor
的 annotation
属性。
使用示例
声明 @MyContextConfiguration
注解,该注解中的 value
和 locations
是彼此的显式别名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented public @interface MyContextConfiguration { @AliasFor("locations") String[] value() default {};
@AliasFor("value") String[] locations() default {}; }
|
简单测试下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class AliasForTest implements WithAssertions {
private static final String PACKAGE = "indi.mofan";
@MyContextConfiguration(PACKAGE) static class Class1 { }
@Test public void testExplicitAliasesWithinAnAnnotation() { MyContextConfiguration annotation = AnnotationUtils.getAnnotation(Class1.class, MyContextConfiguration.class); assertThat(annotation) .isNotNull() .extracting(MyContextConfiguration::locations, as(InstanceOfAssertFactories.array(String[].class))) .contains(PACKAGE); assertThat(annotation) .isNotNull() .extracting(MyContextConfiguration::value, as(InstanceOfAssertFactories.array(String[].class))) .contains(PACKAGE); } }
|
1.3 元注解属性的显式别名
这指的是:如果 @AliasFor
中的 annotation
属性被设置为与它标记的注解的不同注解,那么 attribute
或 value
则被解释为元注解中属性的别名(或者说,元注解中属性的重写)。
这使得能够精准地进行细粒度控制在一个注解层次结构中哪些属性被重写。事实上,甚至可以使用 @AliasFor
注解为元注解的 value
属性声明别名。
使用要求
- 当前注解中,作为元注解中属性别名的属性必须使用
@AliasFor
注解进行标记,并且 attribute
或 value
必须引用元注解中的属性;
- 互为别名的属性必须具有相同的返回类型;
@AliasFor
的 annotation
必须引用元注解;
- 引用的元注解必须被声明在被
@AliasFor
注解标记的注解上。
使用示例
声明 @XmlTestConfig
注解,该注解的一个元注解是 @MyContextConfiguration
,其 xmlFiles
属性是 @MyContextConfiguration
注解中 locations
属性的别名,也就是说 xmlFiles
属性值能够重写 locations
属性值:
1 2 3 4 5 6 7 8 9 10 11 12
|
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @MyContextConfiguration public @interface XmlTestConfig { @AliasFor(annotation = MyContextConfiguration.class, attribute = "locations") String[] xmlFiles(); }
|
简单测试下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @XmlTestConfig(xmlFiles = PACKAGE) static class Class2 { }
@Test public void testExplicitAliasForAttributeInMetaAnnotation() { XmlTestConfig annotation = AnnotationUtils.getAnnotation(Class2.class, XmlTestConfig.class); assertThat(annotation) .isNotNull() .extracting(XmlTestConfig::xmlFiles, as(InstanceOfAssertFactories.array(String[].class))) .contains(PACKAGE); MyContextConfiguration metaAnnotation = AnnotationUtils.getAnnotation(Class2.class, MyContextConfiguration.class); assertThat(metaAnnotation).isNotNull().extracting(MyContextConfiguration::locations, as(InstanceOfAssertFactories.array(String[].class))) .isEmpty(); metaAnnotation = AnnotatedElementUtils.findMergedAnnotation(Class2.class, MyContextConfiguration.class); assertThat(metaAnnotation).isNotNull().extracting(MyContextConfiguration::locations, as(InstanceOfAssertFactories.array(String[].class))) .isNotEmpty().contains(PACKAGE); }
|
实际使用场景测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented public @interface MyMetaAnnotation { String value() default "";
int sort() default Integer.MAX_VALUE; }
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @MyMetaAnnotation public @interface MyAnnotation { @AliasFor(annotation = MyMetaAnnotation.class, attribute = "value") String myValue() default "testValue";
@AliasFor(annotation = MyMetaAnnotation.class, attribute = "sort") int mySort() default 0; }
|
简单测试下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @MyAnnotation static class Class6 { }
@MyAnnotation(myValue = "mofan", mySort = 1) static class Class7 { }
@Test public void testMyAnnotation() { MyMetaAnnotation metaAnnotation = AnnotatedElementUtils.getMergedAnnotation(Class6.class, MyMetaAnnotation.class); assertThat(metaAnnotation).isNotNull() .extracting(MyMetaAnnotation::value, MyMetaAnnotation::sort) .containsExactly("testValue", 0);
metaAnnotation = AnnotatedElementUtils.findMergedAnnotation(Class7.class, MyMetaAnnotation.class); assertThat(metaAnnotation).isNotNull() .extracting(MyMetaAnnotation::value, MyMetaAnnotation::sort) .containsExactly("mofan", 1); }
|
好像和使用示例没啥区别,似乎是在凑字数? 🤸♂
1.4 注解中的隐式别名
这指的是:如果一个注解中的一个或多个属性被声明为同一元注解中属性的属性重写(直接或间接地),那么这些属性将被视为彼此的隐式别名,从而导致类似于注解中的显式别名的行为。
使用要求
- 互为隐式别名的每个属性必须使用
@AliasFor
注解进行标记,并且 attribute
或 value
必须引用同一个元注解中的同一个属性(直接地或通过注解层次结构中的其他显式元注解属性重写进行传递);
- 互为隐式别名的每个属性必须具有相同的返回类型;
- 互为隐式别名的每个属性必须声明默认值,并且声明相同的默认值;
@AliasFor
的 annotation
必须引用元注解;
- 引用的元注解必须被声明在被
@AliasFor
注解标记的注解上。
使用示例
声明 @MyTestConfig
注解,该注解的一个元注解是 @MyContextConfiguration
,其 value
、groovyScripts
和 xmlFiles
属性都是 @MyContextConfiguration
注解中 locations
属性的别名,这三个属性也彼此互为隐式别名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
@MyContextConfiguration @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented public @interface MyTestConfig { @AliasFor(annotation = MyContextConfiguration.class, attribute = "locations") String[] value() default {};
@AliasFor(annotation = MyContextConfiguration.class, attribute = "locations") String[] groovyScripts() default {};
@AliasFor(annotation = MyContextConfiguration.class, attribute = "locations") String[] xmlFiles() default {}; }
|
简单测试下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @MyTestConfig(PACKAGE) static class Class3 { }
@Test public void testImplicitAliasesWithinAnAnnotation() { MyTestConfig annotation = AnnotationUtils.getAnnotation(Class3.class, MyTestConfig.class); assertThat(annotation).isNotNull() .extracting(MyTestConfig::value, as(InstanceOfAssertFactories.array(String[].class))) .containsOnly(PACKAGE); assertThat(annotation).isNotNull() .extracting(MyTestConfig::groovyScripts, MyTestConfig::xmlFiles) .containsAll(Arrays.asList(new String[]{PACKAGE}, (Object) new String[]{PACKAGE}));
MyContextConfiguration mergedAnnotation = AnnotatedElementUtils.findMergedAnnotation(Class3.class, MyContextConfiguration.class); assertThat(mergedAnnotation).isNotNull() .extracting(MyContextConfiguration::locations, as(InstanceOfAssertFactories.array(String[].class))) .containsOnly(PACKAGE); }
|
注解中的传递隐式别名
声明 @GroovyOrXmlTestConfig
注解,该注解的一个元注解是 @MyTestConfig
,其中的 groovy
属性是 @MyTestConfig
注解的 groovyScripts
属性的显式重写,而 xml
属性是 @MyContextConfiguration
注解的 locations
属性的显式重写。此外,groovy
和 xml
是彼此的传递隐式别名,因为它们都有效地重写了 @MyContextConfiguration
注解中的 locations
属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
@MyTestConfig @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented public @interface GroovyOrXmlTestConfig { @AliasFor(annotation = MyTestConfig.class, attribute = "groovyScripts") String[] groovy() default {};
@AliasFor(annotation = MyContextConfiguration.class, attribute = "locations") String[] xml() default {}; }
|
简单测试下:
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
| @GroovyOrXmlTestConfig(groovy = PACKAGE) static class Class4 { }
@GroovyOrXmlTestConfig(xml = PACKAGE) static class Class5 { }
@Test public void testTransitiveImplicitAliasesWithinAnAnnotation() { GroovyOrXmlTestConfig groovyOrXmlTestConfig = AnnotationUtils.getAnnotation(Class4.class, GroovyOrXmlTestConfig.class); assertThat(groovyOrXmlTestConfig).isNotNull() .extracting(GroovyOrXmlTestConfig::groovy, as(InstanceOfAssertFactories.array(String[].class))) .containsOnly(PACKAGE);
MyTestConfig myTestConfig = AnnotatedElementUtils.findMergedAnnotation(Class4.class, MyTestConfig.class); assertThat(myTestConfig).isNotNull() .extracting(MyTestConfig::groovyScripts, MyTestConfig::value, MyTestConfig::xmlFiles) .containsAll(Arrays.asList(new String[]{PACKAGE}, new String[]{PACKAGE}, new String[]{PACKAGE}));
MyContextConfiguration myContextConfiguration = AnnotatedElementUtils.findMergedAnnotation(Class4.class, MyContextConfiguration.class); assertThat(myContextConfiguration).isNotNull() .extracting(MyContextConfiguration::locations, as(InstanceOfAssertFactories.array(String[].class))) .containsOnly(PACKAGE);
myContextConfiguration = AnnotatedElementUtils.findMergedAnnotation(Class5.class, MyContextConfiguration.class); assertThat(myContextConfiguration).isNotNull() .extracting(MyContextConfiguration::locations, as(InstanceOfAssertFactories.array(String[].class))) .containsOnly(PACKAGE); }
|
1.5 注意事项与总结
同时使用互为别名的多个属性时,要求这些属性值必须都一样,否则抛出 AnnotationConfigurationException
异常:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @MyContextConfiguration(value = PACKAGE, locations = PACKAGE) static class Class8 { }
public static final String ANOTHER_PACKAGE = "com.mofan";
@MyContextConfiguration(value = PACKAGE, locations = ANOTHER_PACKAGE) static class Class9 { }
@Test public void testRepeatedlyUsingAliases() { assertThatNoException().isThrownBy(() -> AnnotatedElementUtils.getMergedAnnotation(Class8.class, MyContextConfiguration.class)); assertThatThrownBy(() -> AnnotatedElementUtils.getMergedAnnotation(Class9.class, MyContextConfiguration.class)) .isInstanceOf(AnnotationConfigurationException.class); }
|
了解 @AliasFor
注解的使用方式后,可以概括其作用:
- 指定别名,属性互换;
- 将一个注解的属性值传递给另一个注解的属性值,对后者的属性值进行覆盖;
- 组合多个注解,使得一个注解实现多个注解的效果
2. Java 中的一些元注解
2.1 @Inherited
Inherited
意为“继承”,读音为 [ɪnˈherɪtɪd]
。
@Inherited
注解是在 Java 初次引入注解时就存在的:
1 2 3 4 5
| @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Inherited { }
|
它只能作用在注解上,因此是一个元注解。
@Inherited
使得子类能够继承父类上的被 @Inherited
注解标记的注解, 这里的父类不包括接口。
声明两个注解,它们都被 @Inherited
注解标记:
1 2 3 4 5 6 7 8 9 10 11
| @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Inherited @interface InheritedA { }
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Inherited @interface InheritedB { }
|
声明的两个注解一个标记抽象类,一个标记接口:
1 2 3 4 5 6 7
| @InheritedA static abstract class AbstractClass1 { }
@InheritedB interface Interface1 { }
|
声明子类 Class1
,使其继承抽象类、实现接口:
1 2
| static class Class1 extends AbstractClass1 implements Interface1 { }
|
简单测试下:
1 2 3 4 5 6 7
| @Test public void testInherited() { Annotation[] annotations = Class1.class.getAnnotations(); assertThat(annotations).isNotEmpty() .hasSize(1) .hasOnlyElementsOfTypes(InheritedA.class); }
|
2.2 @Repeatable
Repeatable
意为“可重复的”,读音为 [rɪˈpiːtəbl]
。
Java 并不直接支持在同一个位置声明两个相同的注解,为了解决这个问题,在 JDK 8 中引入了 @Repeatable
注解:
1 2 3 4 5 6
| @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Repeatable { Class<? extends Annotation> value(); }
|
它只能作用在注解上,因此也是一个元注解。
使用 @Repeatable
注解时,必须引用另外一个注解,引用的注解是当前注解的复数形式。
声明 @Repeat
和 @Repeats
注解,前者被 @Repeatable
注解标记,并使 @Repeatable
注解的 value
属性引用 @Repeats
注解:
1 2 3 4 5 6 7 8 9 10 11 12
| @Repeatable(Repeats.class) @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @interface Repeat { String value(); }
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @interface Repeats { Repeat[] value(); }
|
后续使用 @Repeat
注解时,能够在同一个类上声明多个 @Repeat
注解:
1 2 3 4
| @Repeat("mofan") @Repeat("默烦") static class Class2 { }
|
如果 @Repeat
注解没有被 @Repeatable
注解标记,那么会编译报错。
也可以使用 @Repeats
注解,其 value
属性引用多个 @Repeat
注解:
1 2 3 4 5 6
| @Repeats({ @Repeat("mofan"), @Repeat("默烦") }) static class Class3 { }
|
简单测试下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Test public void testRepeatable() { String[] values = {"mofan", "默烦"}; Annotation[] annotations = Class2.class.getAnnotations(); assertThat(annotations).hasSize(1) .hasOnlyElementsOfType(Repeats.class) .extracting(i -> ((Repeats) i).value()) .flatMap(i -> Arrays.stream(i).collect(Collectors.toList())) .extracting(Repeat::value) .contains(values); assertThat(Class2.class.isAnnotationPresent(Repeat.class)).isFalse();
annotations = Class3.class.getAnnotations(); assertThat(annotations).hasSize(1) .hasOnlyElementsOfType(Repeats.class) .extracting(i -> ((Repeats) i).value()) .flatMap(i -> Arrays.stream(i).collect(Collectors.toList())) .extracting(Repeat::value) .contains(values); }
|