封面来源:碧蓝航线 湮烬尘墟 活动 CG

本文涉及的代码:

1. @AliasFor 注解

1.1 @AliasFor 的简介

Java 中的注解是在 JDK 1.5 引入的,注解的出现极大地简化了程序员对 Java 程序的描述,但其中也存在一些缺陷,比如注解彼此之间没有继承关系、无法将一个注解上的属性值传递给另一个注解,为了解决这些问题,Spring 引入了 @AliasFor 注解。

顾名思义,@AliasFor 注解是用于取别名的,能够将一个注解上的属性值传递给另一个注解,或者让同一个注解中的两个属性互为别名。注意, 这些特性不是 Java 原生支持的, 因此需要额外使用 AnnotationUtilsAnnotatedElementUtils 工具类来解析。

@AliasFor 适用于三种场景:

  1. 注解中的显式别名
  2. 元注解属性的显式别名
  3. 注解中的隐式别名

1.2 注解中的显式别名

这指的是:在同一个注解中的不同属性上使用 @AliasFor 注解,表示它们是彼此可互换的别名。

使用要求

  • 组成别名的每个属性都应该被 @AliasFor 注解标记,并且 @AliasFor 中的 attributevalue 属性必须引用互为别名的属性中的另一个属性。从 Spring 5.2.1 开始,可以只标记别名对中的一个属性,但为了更好的兼容性,还是建议标记别名对中的两个属性;
  • 互为别名的属性必须具有相同的返回类型;
  • 互为别名的属性必须声明默认值,并且声明相同的默认值;
  • 不使用 @AliasForannotation 属性。

使用示例

声明 @MyContextConfiguration 注解,该注解中的 valuelocations 是彼此的显式别名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author mofan
* @date 2023/3/27 11:01
*/
@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 属性被设置为与它标记的注解的不同注解,那么 attributevalue 则被解释为元注解中属性的别名(或者说,元注解中属性的重写)。

这使得能够精准地进行细粒度控制在一个注解层次结构中哪些属性被重写。事实上,甚至可以使用 @AliasFor 注解为元注解的 value 属性声明别名。

使用要求

  • 当前注解中,作为元注解中属性别名的属性必须使用 @AliasFor 注解进行标记,并且 attributevalue 必须引用元注解中的属性;
  • 互为别名的属性必须具有相同的返回类型;
  • @AliasForannotation 必须引用元注解;
  • 引用的元注解必须被声明在被 @AliasFor 注解标记的注解上。

使用示例

声明 @XmlTestConfig 注解,该注解的一个元注解是 @MyContextConfiguration,其 xmlFiles 属性是 @MyContextConfiguration 注解中 locations 属性的别名,也就是说 xmlFiles 属性值能够重写 locations 属性值:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @author mofan
* @date 2023/3/27 13:40
*/
@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 注解进行标记,并且 attributevalue 必须引用同一个元注解中的同一个属性(直接地或通过注解层次结构中的其他显式元注解属性重写进行传递);
  • 互为隐式别名的每个属性必须具有相同的返回类型;
  • 互为隐式别名的每个属性必须声明默认值,并且声明相同的默认值;
  • @AliasForannotation 必须引用元注解;
  • 引用的元注解必须被声明在被 @AliasFor 注解标记的注解上。

使用示例

声明 @MyTestConfig 注解,该注解的一个元注解是 @MyContextConfiguration,其 valuegroovyScriptsxmlFiles 属性都是 @MyContextConfiguration 注解中 locations 属性的别名,这三个属性也彼此互为隐式别名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author mofan
* @date 2023/3/27 18:06
*/
@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 属性的显式重写。此外,groovyxml 是彼此的传递隐式别名,因为它们都有效地重写了 @MyContextConfiguration 注解中的 locations 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author mofan
* @date 2023/3/27 18:16
*/
@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();
// 两个 @Repeat 注解会合并成一个 @Repeats
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);
// 两个 @Repeat 合并成一个 @Repeats 注解后,@Repeat 相当于不存在
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);
}