Java 元注解及 Spring 组合注解应用

Java 1.5(Tiger) 个人认为最为激动人心的两个特性是泛型与注解(Java Versions, Features and History)。泛型自然是不必说了,注解对 Java 世界的改变比泛型伟大的多(现在框架的注解配置),在 Java 1.5 之前我们只能在 Javadoc 注释中做文章,于是只能用 XDoclet 那样不伦不类的东西。Java 的注解发展到现在几乎可以使用在书写代码时的任何地方,见 java.lang.annotation.ElementType 中的类型,囊括了 TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE, TYPE_PARAMETER(since 1.8), TYPE_USE(since 1.8)。

Java 1.5 基本确定了注解的基本框架,包括元注解(meta-annotation); 直到 Java 8 又扩展了注解的使用范围,列举如下:

创建类实例

new@Interned MyObject();

类型映射

myString = (@NonNull String) str;

implements 语句中

class UnmodifiableList<T> implements@Readonly List<@Readonly T> { ... }

throw exception声明

void monitorTemperature() throws@Critical TemperatureException { ... }

解析前面 ElementType Java 8 增加的 TYPE_PARAMETER和 TYPE_USE 注解使用新场合。ElementType.TYPE_PARAMETER 表示该注解能写在类型变量的声明语句中。ElementType.TYPE_USE 表示该注解能写在使用类型的任何语句中(如: 声明语句、泛型和强制转换语句中的类型)

另外就是更方便使用的重复注解 -- @Repeatable

本文不会讲解 Java 注解的基本知识和创建自定的注解,主要关注标题中的 Java 元注解及 Spring 对元注解的广泛应用 -- 即 Spring 组合注解

Java 元注解(meta-annotation)

所谓的元注解(meta-annotation) 也就是注解的注解,注解本身就是元数据,像 meta-data。具体到 Java 的注解就是注解可以应用到别的注解上去,@Target 包含了 ANNOTATION_TYPE。所以在我们定义普通注解时用到的 @Retention, @Target, @Documented, @Inherited, @Repeatable 就是一拨 Java 内置元注解,下面是 @Target 的定义

1@Documented
2@Retention(RetentionPolicy.RUNTIME)
3@Target(ElementType.ANNOTATION_TYPE)
4public @interface Retention {
5    RetentionPolicy value();
6}

体验一下 @Inherited 注解的作用

@Inherited 标明注解是能够被传递到子类的,即注解在父类的注解也会作用到它的子类上去,比如 Spring 的 @Transactional 注解就被 @Inherited 标识了

1@Target({ElementType.METHOD, ElementType.TYPE})
2@Retention(RetentionPolicy.RUNTIME)
3@Inherited
4@Documented
5public @interface Transactional {
6    ......
7}

具体表现就是

1@Transactional
2public class BaseRepository {
3    ......
4}
5
6public class UserRepository extends BaseRepository {
7    ......
8}

UserRepository 继承了 BaseRepository, 所以 UserRepository 也就启用了事物。

自己来一下

 1@Target({ElementType.TYPE})
 2@Retention(RetentionPolicy.RUNTIME)
 3@Inherited
 4@interface BB {
 5}
 6
 7@BB
 8class Parent {
 9}
10
11class Child extends Parent {
12}
13
14public class Test {
15    public static void main(String[] args) {
16        BB bb = Child.class.getAnnotation(BB.class);
17        System.out.println(bb); //BB 注解没有 @Inherited 的话,bb 将为 null
18    }
19}

在注解中如果去掉 @Inherited, 上面的 Child.class.getAnnotation(BB.class) 将返回 null.

要是换成实现一个接口,事情就不一样了

1@BB
2interface Parent {
3}
4
5class Child implements Parent {
6}
7
8//Child.class.getAnnotation(BB.class) 总是为 null 

Child 不管是在 @BB 有没有 @Inherited 标识都继承不到 @BB 注解。

@Target({ElementType.ANNOTATION_TYPE})

ElementType.ANNOTATION_TYPE 的用处,好像也就是一个约束,只限定被它声明了注解只能用于其他的注解类型上去,看下面的图片

User 类上的 @BB 处报错: '@BB' not applicable to type. 就是说 @Target 为 ElementType.ANNOTATION_TYPE 的注解只能用于其他注解上,而 @Target 为 ElementType.TYPE 可以用在许多地方,类,接口,枚举或其他注解上。除此之外,ElementType.ANNOTATION_TYPE 也没别的太多意思,它与下面的组合注解没有什么关系。

Spring 中的元注解与组合注解

再重新回味一下,Java 原生的元注解(Meta-annotations) 基本就是指内置的 @Retention, @Target, @Documented, @Inherited, @Repeatable 那一干注解,以及自定义注解加上 @Target({ElementType.ANNOTATION_TYPE}) 实现的自定义元注解。而 Spring 的元注解概念是不一样的,它认为能用于注解的注解就是元注解,即 @Target({ElementType.Type}) 标识的也是元注解,因为其他的注解也是 Type.

Spring 中元注解与组合注解又是很紧密的两个概念,从官方的文档中

1@Target({ElementType.TYPE})
2@Retention(RetentionPolicy.RUNTIME)
3@Documented
4@Component
5public @interface Service {
6    String value() default "";
7}

由于 @Component 注解到了 @Service 注解,所以这里的 @Component 称之元注解,而 @Service 而称之为组合注解,即组合了 @Component 的注解,当然还能组合更多的元注解。在 Spring 中有大量组合注解的例子,像 @GetMapping

1@Target({ElementType.METHOD})
2@Retention(RetentionPolicy.RUNTIME)
3@Documented
4@RequestMapping(     //这里的 @RequestMapping 就是 @GetMapping 的元注解
5    method = {RequestMethod.GET}
6)
7public @interface GetMapping {
8    ....
9}

为什么说 @Component 和 @RequestMapping 不是一般意义上 Java 的元注解呢,只要查看下 @Component 的定义就知道,

1@Target({ElementType.TYPE})    //它的 Target 并不需要有 ElementType.ANNOTATION_TYPE, 这不并妨碍它应用于别的注解上
2@Retention(RetentionPolicy.RUNTIME)
3@Documented
4public @interface Component {
5    String value() default "";
6}

创建自己的组合注解

比如想要声明一个 Lazy 的 Bean,在 JavaConfig 方法上要同时用到两个注解 @Lazy 和 @Bean

1@Lazy
2@Bean(name = "newName")
3public String testLazyBean() {
4    return "hello world";
5}

那么我们是否能创建一个组合注解,只用一个注解就能声明出一个 Lazy 的 Bean 来呢,没问题,就是下面的 @LazyBean

1@Target({ElementType.METHOD})
2@Retention(RetentionPolicy.RUNTIME)
3@Bean
4@Lazy
5public @interface LazyBean {
6
7    @AliasFor(annotation = Bean.class, attribute = "value")
8    String[] name() default {};
9}

然后前面的声明 Bean 的代码就能够写成

1@LazyBean(name = "newName")
2public String testLazyBean() {
3    return "hello world";
4}

节约了一行代码,简洁明了,表现力也增强了。如果需要组合更多的元注解时,代码效果上就会更佳了。

从 Spring 组合注解的结果推导出它的内部实现,被 @LazyBean 标的 bean, 相当于同时被 @Bean 和 @Lazy 标注了。

借机加强理解一下 Spring 中元注解与组合注解的概念:

  1. 这儿的 @Bean 和 @Lazy 是用来注解 @LazyBean 的,所以 @Bean 和 @Lazy  称之为元注解
  2. @LazyBean 是由 @Bean 和  @Lazy 组合而成的,因此 @LazyBean 就是一个组合注解

Spring 的组合注解是如何工作的

如果使用正常的 Java 注解反射 API getAnnotation()isAnnotationPresent() 只能发现组合后的注解 @LazyBean, 而元注解是这样得不到的。但 Spring 在发现注解的时候走入的更深,注解的注解(元注解)和组合后的注解都能捞出来。看下面的例子

 1@Retention(RetentionPolicy.RUNTIME)
 2@Target({ElementType.TYPE})
 3@interface AA {
 4}
 5
 6@Retention(RetentionPolicy.RUNTIME)
 7@Target({ElementType.TYPE})
 8@interface BB {
 9}
10
11@Retention(RetentionPolicy.RUNTIME)
12@AA
13@BB
14@interface CC {
15}
16
17@CC
18class User {
19}
20
21public class Test {
22    public static void main(String[] args) {
23        User.class.getAnnotation(CC.class);       // @yanbin.blog.CC()
24        User.class.getAnnotation(AA.class);       // null
25        User.class.getAnnotation(BB.class);       // null
26
27        User.class.isAnnotationPresent(CC.class); //true
28        User.class.isAnnotationPresent(AA.class); //false
29        User.class.isAnnotationPresent(BB.class); //false
30    }
31}

上面的 @CC 由 @AA 和 @BB 组合而成,用 @CC 注解到 User 类上,常规的 Java 反射类只能找到 @CC,要反射出 @AA 和 @BB,必须对 @CC 类进一步反射。而 Spring 提供了 AnnotationUtilsAnnotatedElementUtils 工具类来查找注解类,如下

 1AnnotationUtils.findAnnotation(User.class, CC.class);               // @yanbin.blog.CC()
 2AnnotationUtils.findAnnotation(User.class, AA.class);               // @yanbin.blog.AA()
 3AnnotationUtils.findAnnotation(User.class, BB.class);               // @yanbin.blog.BB()
 4
 5AnnotationUtils.isAnnotationMetaPresent(CC.class, AA.class);        // true
 6AnnotationUtils.isAnnotationMetaPresent(CC.class, BB.class);        // true
 7
 8AnnotatedElementUtils.getMergedAnnotation(User.class, CC.class);    // @yanbin.blog.CC()
 9AnnotatedElementUtils.getMergedAnnotation(User.class, AA.class);    // @yanbin.blog.AA()
10AnnotatedElementUtils.getMergedAnnotation(User.class, BB.class);    // @yanbin.blog.BB()
11
12AnnotatedElementUtils.findMergedAnnotation(User.class, CC.class);   // @yanbin.blog.CC()
13AnnotatedElementUtils.findMergedAnnotation(User.class, AA.class);   // @yanbin.blog.AA()
14AnnotatedElementUtils.findMergedAnnotation(User.class, BB.class);   // @yanbin.blog.BB()

如某个类(User) 被某个组合注解(@CC) 修饰了,Spring 认该类(User) 被组成 @CC 的所有元注解(@AA 和 @BB) 修饰了。

组合注解时的属性值传递与覆盖 

如果组合注解的元注解有属性值时,直接写就行了,例如:

1@Target({ElementType.METHOD})
2@Retention(RetentionPolicy.RUNTIME)
3@Bean("FixedLazyBean")
4@Lazy
5public @interface LazyBean {
6}

用上面的组合注解 @LazyBean 标注到某个方法上,注册 SpringBean 时的名称就是 @Bean("FixedLazyBean") 中的 "FixedLazyBean"。

假如在使用 @LazyBean 注解时还要能够动态指定 Spring bean 名称,那么 @LazyBean 中就需要一个属性覆盖 @Bean 的 value 属性(或者说传递给 @Bean 的 value 属性),这时修又要用到一个 Spring 特定的注解 @AliasFor -- Spring 4.2 新加的特性(New Features and Enhancements in Spring Framework 4.2)。对合并属性的获得也是用 AnnotatedElementUtils 中的方法 findMergedAnnotationAttributes(...), 还是看例子:

1@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
2@Retention(RetentionPolicy.RUNTIME)
3@Bean
4@Lazy
5public @interface LazyBean {
6
7    @AliasFor(annotation = Bean.class, attribute = "name")
8    String[] name() default {};      //@LazyBean 的 name 值传递给元注解 @Bean 的 name 属性
9}

应用 @LazyBean 时指定 Bean name 

1@LazyBean(name = "newName")
2public String testLazyBean() {
3    return "hello";
4}

这样就会向 Spring 上下文中注册一个名称为 "newName" 的字符串。

Spring 的 @AliasFor 使用时有不少要求,请参见它的 Javadoc 文档 Annotation Type AliasFor. 创建一个组合注解后最好在使用之前测试它是否完全达到预期,尤其是在覆盖注解的 value 属性时要多加留意。

链接:

  1. Java 8 新特性:扩展注解(类型注解和重复注解)
  2. 1.10.2. Using Meta-annotations and Composed Annotations
  3. Implementing custom annotations for Spring MVC
  4. Spring Annotation Programming Model
  5. Spring 4 Meta Annotations
永久链接 https://yanbin.blog/java-spring-meta-annotation/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。