浅谈组合注解 & 注解别名

Otstar Lin

前言

这篇文章很早就躺在了草稿里了,一直没有写 2333,最近在考试,因为都是一些相对简单的考试,同时又暂停的项目的开发,所以最近相对较闲,便打算把这个坑填一下。

什么是组合注解和注解别名?

如果你看过 Spring 的注解的源码,那么这两个概念一定不会陌生。

注解别名 指的注解的属性拥有别名的功能,让多个属性值表达同一个意思,如 Spring 的 @Bean 注解:

从代码中看到 value 属性和 name 属性是相同的,设置 valuename 任意一个值都代表了设置了 Bean 的名称。这就是注解别名。

组合注解 简单来说就是 Spring 自行实现的将多个注解组合到一个注解上的功能。如 Spring 里的 @RestController 注解:

从代码中看到,@RestController 注解上标记了 @Controller@ResponseBody 注解,这样 @RestController 就组合了 @Controller@ResponseBody 的功能。

除了 组合注解注解别名,Spring 还提供了类似于类继承的 注解继承 功能,比如 @RestControllervalue 属性上标记了 @AliasFor(annotation = Controller.class),此时若设置了 @RestContollervalue 属性,则代表设置了 @Contollervalue 属性。

Spring 是如何实现的?

首先我们先随便写一个 Demo:

启动调试会话,一路步入就可以看到以下的代码段:

可以看到,Spring 在创建 MergedAnnotation 前会先获取 AnnotationTypeMappings,该对象保存了 MergedAnnotation.from 传入的注解及其所有父注解(非 JDK 注解)的 AnnotationTypeMapping 信息,AnnotationTypeMapping 里保存了根注解(Spring 保存的方式是自下向上的,所以这个的根注解是 from 传入的注解),源注解(下一级注解,如 RequestMapping 的源注解是 GetMapping),别名索引数组,别名指向的方法等信息。然后获取当前传入注解的 AnnotationTypeMapping 组成 MergedAnnotation

不过有了 MergedAnnotation 那么如何把 MergedAnnotation 变成 Java 的注解呢?如果直接返回通过反射获取的注解,那么别名和子注解的值传上来的值就无法被更改,所以为了获得 Java 的注解,Spring 会重新创建该注解的注解代理类。

如果你看过 Java 注解的源码,Java 注解其实是通过 JDK 代理实现的,通过 JDK 代理从 AnnotationInvocationHandler 里的 memberValues Map 获取注解值。Spring 就是使用类似的方式通过 MergedAnnotation.synthesize 方法调用了 SynthesizedMergedAnnotationInvocationHandler.createProxy 动态创建了 Java 注解,而 SynthesizedMergedAnnotationInvocationHandler 包装了 MergedAnnotationAttributeMethods

到此 Spring 处理组合注解的原理的关键部分就差不多讲完了,至于获取值部分就不说明了,只是简单的取到属性对应的方法,然后利用反射获取值。

实现

既然知道了原理,那么就可以自行实现一个组合注解 & 注解别名了。

方式

在进行编码前我们要先确定我们组合注解的实现方式。Spring 的实现方式较为复杂,所以我们不采用 Spring 的实现方法,而是直接对 Java 注解里的 memberValues Map 动手,通过修改这个 Map 的值,我们就可以修改注解的属性值。不过需要注意,Java 注解是单例的,所以我们不能直接修改从反射获取的注解里的 memberValues,而是要克隆一份,另外使用 JDK 动态代理创建注解对象。

20210109222156

代码

首先我们先准备 @AliasFor 别名注解:

为了方便实现重复注解我添加了一个 @RepeatItem 注解:

然后就是注解工具类的一些基础方法:

为了创建注解代理类,我们还需要一个 InvocationHandler 用于代理属性方法,我懒得写了就直接把 JDK 里的 AnnotationInvocationHandler 拷贝了出来(JDK 里的是私有的不方便使用),由于代码太长了,这里就不贴了,可以到 Github 上查看。

然后就是相应的代理工具方法:

然后就是最关键的合并注解值的方法了:

梳理下流程会更方便阅读:

首先我们要明确一点,注解的处理顺序是先子注解,然后父注解。

  1. 获取原始 memberValues 的 Map
  2. 创建克隆的空 memberValues
  3. 循环遍历原始 memberValues,取出每一项的值
  4. 通过属性名称从注解 Class 查找 Method,然后取得 @AliasFor 的注解
  5. 如果子注解有传上来覆盖的值,那么就使用这个值(overwriteMap 里存放)
  6. 否则看下有没有设置 @AliasFor 注解及 value 值(别名)如果设置了,则判断是否是默认值(Method 可以获取方法默认值,也就是属性默认值),如果不是默认值,那么这个值就是被设置过的,此时就不应该覆盖它。
  7. 如果设置了 @AliasFor 注解同时不是默认值,那么就获取别名的值(注意这个别名的值也有可能是子注解传上来的,所以需要判断下 overwriteMap 里有没有设置)
  8. 最后将获得的最终的值存入克隆的 memberValues
  9. 然后处理要传到父注解的值,如果设置了 @AliasForannotation 值,那么就将这个值设置到对应的 overwriteMap 里,这样父注解在处理的时候就可以获取到这个值了

有了处理注解值的方法,那么就可以写遍历注解的方法了:

照样写个流程吧:

  1. 因为传入的是可被标注的元素,所以就取出这个元素所标注的所有注解,遍历
  2. 如果是 JDK 注解就直接跳过,因为这些注解对我们的程序无用,而且会导致无限递归
  3. 接着就是使用 AnnotatedElementgetAnnotationsByType 方法获取标注在元素上的所有指定注解类型的值。之所以要这么做是因为 Java 支持重复注解,通过 getAnnotations 的方法只能取得一个同类型的注解
  4. 取得注解后就调用 mergeAnnotationValue 方法合并注解值,同时把要传到父注解的值存入 overwriteMap
  5. 除了 Java 标准的重复注解,我们还常用带 s 的注解包裹来做到重复注解,如 @MapperScans@MapperScan 此时就需要特殊处理,这里使用的方法是在带 s 的注解里添加一个 @RepeatItem 注解,然后该注解存储单个注解的类型,这样就能把带 s 注解里的单个注解存到它对应类型的注解里。
  6. 处理完当前注解后就接着处理父注解(递归处理)

遍历完所有的注解后,就取得了可标注元素的所有注解处理过的 memberValues,此时就可以将 memberValues 转成注解了。

此时就有了标注在可标注元素上所有的注解了,有了这些就可以封装成组合注解了:

首先是 MergedAnnotation 的接口:

然后就是实现类:

到这里我们自己的组合注解就实现完成了,让我们来使用下吧:

结语

组合注解简单化注解配置,用很少的注解来标注特定含义的多个元注解,同时提供了很好的扩展性,可以根据实际需要灵活的自定义注解。经过组合注解的重构后,我们就不再需要写很多注解处理器,避免了重复,同时也不易出错。