Spring 中的 @AliasFor 注解

1、概览

本文将会带你了解 Spring 中的 @AliasFor 注解。

首先介绍一些框架中的使用示例,最后看看如何在自定义注解中使用 @AliasFor

2、注解

@AliasFor 自 4.2 版起成为框架的一部分。多个 Spring 核心注解已更新为包含此注解。

可以将其用于装饰单个注解中的属性,或者在由元注解组成的注解中使用。元注解是可以应用于其他注解的注解。

在同一个注解中,使用 @AliasFor 来声明属性的别名,这样就可以交替使用它们。或者,可以在组合注解中使用 @AliasFor 来覆盖元注解中的属性。换句话说,当使用 @AliasFor 在组合注解中装饰属性时,它会覆盖其元注解中的指定属性。

许多核心 Spring 注解,如 @Bean@ComponentScan@Scope@RequestMapping@RestController 现在都使用 @AliasFor 来配置其内部属性别名。

@AliasFor 注解的定义如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface AliasFor {
    @AliasFor("attribute")
    String value() default "";
    
    @AliasFor("value")
    String attribute() default "";

    Class<? extends Annotation> annotation() default Annotation.class;
}

既可以隐式地使用这个注解,也可以显式地使用它。隐式使用仅限于注解内部的别名。相比之下,显式使用还可以用于元注解中的属性。

3、注解中的显示别名

参考 Spring 的核心注解 @ComponentScan

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {

    @AliasFor("basePackages")
    String[] value() default {};

    @AliasFor("value")
    String[] basePackages() default {};
...
}

可以看到,value 在这里被明确定义为 basePackages 的别名,反之亦然。这意味着可以交替使用它们。

因此,下面这两种用法实际上是一样的:

@ComponentScan(basePackages = "com.baeldung.aliasfor")

@ComponentScan(value = "com.baeldung.aliasfor")

此外,由于这两个属性都被标记为 default,所以可以使用简写:

@ComponentScan("com.baeldung.aliasfor")

另外,对于这种情况,Spring 还有一些实现要求。别名属性应声明相同的默认值和返回类型。否则,框架就会抛出 AnnotationConfigurationException 异常。

4、显式地为元注解中的属性设置别名

接着,来看一个元注解的示例。

使用框架的 @RequestMapping 注解作为元注解:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
    String name() default "";
    
    @AliasFor("path")
    String[] value() default {};

    @AliasFor("value")
    String[] path() default {};

    RequestMethod[] method() default {};
    ...
}

创建一个组合注解 @MyMapping,使用框架的 @RequestMapping 注解作为元注解:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@RequestMapping
public @interface MyMapping {
    @AliasFor(annotation = RequestMapping.class, attribute = "method")
    RequestMethod[] action() default {};
}

你可以看到,在 @MyMapping 中,action@RequestMapping 中属性 method 的显式别名。也就是说,组合注解中的 action 重写了元注解中的 method

与注解中的别名类似,元注解属性别名也必须具有相同的返回类型。例如,在本例中就是 RequestMethod[]。另外,属性 annotation 应引用元注解,在本例中就是 annotation = RequestMapping.class

接下来,创建一个名为 MyMappingController 的 Controller 类,使用自定义注解来进行演示。

如下,这里只为 @MyMapping 添加两个属性:routeaction

@Controller
public class MyMappingController {

    @MyMapping(action = RequestMethod.PATCH, route = "/test")
    public void mappingMethod() {}
    
}

最后,测试这个 Controller,看看自定义的组合注解是否生效:

@Test
public void givenComposedAnnotation_whenExplicitAlias_thenMetaAnnotationAttributeOverridden() {
    for (Method method : controllerClass.getMethods()) {
        if (method.isAnnotationPresent(MyMapping.class)) {
            MyMapping annotation = AnnotationUtils.findAnnotation(method, MyMapping.class);
            RequestMapping metaAnnotation = 
              AnnotationUtils.findAnnotation(method, RequestMapping.class);

            assertEquals(RequestMethod.PATCH, annotation.action()[0]);

            assertEquals(0, metaAnnotation.method().length);
        }
    }
}

测试通过,自定义注解的 action 属性成功地覆盖了元注解 @RequestMappingmethod 属性。

5、注解中的隐式别名

为了理解这一点,在 @MyMapping 中添加一些别名:

@AliasFor(annotation = RequestMapping.class, attribute = "path")
String[] value() default {};

@AliasFor(annotation = RequestMapping.class, attribute = "path")
String[] mapping() default {};
    
@AliasFor(annotation = RequestMapping.class, attribute = "path")
String[] route() default {};

在这种情况下,valuemappingroute@RequestMappingpath 的显式元注解重载。因此,它们也是彼此的隐式别名。换句话说,对于 @MyMapping,可以交替使用这三个属性。

还是使用上节中的 Controller 进行测试:

@Test
public void givenComposedAnnotation_whenImplictAlias_thenAttributesEqual() {
    for (Method method : controllerClass.getMethods()) {
        if (method.isAnnotationPresent(MyMapping.class)) {
            MyMapping annotationOnBean = 
              AnnotationUtils.findAnnotation(method, MyMapping.class);

            assertEquals(annotationOnBean.mapping()[0], annotationOnBean.route()[0]);
            assertEquals(annotationOnBean.value()[0], annotationOnBean.route()[0]);
        }
    }
}

注意,没有在 Controller 方法的注解中定义 valuemapping 属性。不过,它们仍然隐式地携带与 route 相同的值。

6、总结

本文介绍了 Spring 框架中 @AliasFor 注解的显式和隐式使用场景,以及如何在自定义注解中使用 @AliasFor 注解来覆盖元注解或当前注解中的属性。


Ref:https://www.baeldung.com/spring-aliasfor-annotation