前言
这篇文章很早就躺在了草稿里了,一直没有写 2333,最近在考试,因为都是一些相对简单的考试,同时又暂停的项目的开发,所以最近相对较闲,便打算把这个坑填一下。
什么是组合注解和注解别名?
如果你看过 Spring 的注解的源码,那么这两个概念一定不会陌生。
注解别名 指的注解的属性拥有别名的功能,让多个属性值表达同一个意思,如 Spring 的 @Bean
注解:
从代码中看到 value
属性和 name
属性是相同的,设置 value
和 name
任意一个值都代表了设置了 Bean 的名称。这就是注解别名。
组合注解 简单来说就是 Spring 自行实现的将多个注解组合到一个注解上的功能。如 Spring 里的 @RestController
注解:
从代码中看到,@RestController
注解上标记了 @Controller
和 @ResponseBody
注解,这样 @RestController
就组合了 @Controller
和 @ResponseBody
的功能。
除了 组合注解 和 注解别名,Spring 还提供了类似于类继承的 注解继承 功能,比如 @RestController
的 value
属性上标记了 @AliasFor(annotation = Controller.class)
,此时若设置了 @RestContoller
的 value
属性,则代表设置了 @Contoller
的 value
属性。
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
包装了 MergedAnnotation
和 AttributeMethods
。
到此 Spring 处理组合注解的原理的关键部分就差不多讲完了,至于获取值部分就不说明了,只是简单的取到属性对应的方法,然后利用反射获取值。
实现
既然知道了原理,那么就可以自行实现一个组合注解 & 注解别名了。
方式
在进行编码前我们要先确定我们组合注解的实现方式。Spring 的实现方式较为复杂,所以我们不采用 Spring 的实现方法,而是直接对 Java 注解里的 memberValues
Map 动手,通过修改这个 Map 的值,我们就可以修改注解的属性值。不过需要注意,Java 注解是单例的,所以我们不能直接修改从反射获取的注解里的 memberValues
,而是要克隆一份,另外使用 JDK 动态代理创建注解对象。
代码
首先我们先准备 @AliasFor
别名注解:
为了方便实现重复注解我添加了一个 @RepeatItem
注解:
然后就是注解工具类的一些基础方法:
为了创建注解代理类,我们还需要一个 InvocationHandler
用于代理属性方法,我懒得写了就直接把 JDK 里的 AnnotationInvocationHandler
拷贝了出来(JDK 里的是私有的不方便使用),由于代码太长了,这里就不贴了,可以到 Github 上查看。
然后就是相应的代理工具方法:
然后就是最关键的合并注解值的方法了:
梳理下流程会更方便阅读:
首先我们要明确一点,注解的处理顺序是先子注解,然后父注解。
- 获取原始
memberValues
的 Map - 创建克隆的空
memberValues
- 循环遍历原始
memberValues
,取出每一项的值 - 通过属性名称从注解
Class
查找Method
,然后取得@AliasFor
的注解 - 如果子注解有传上来覆盖的值,那么就使用这个值(
overwriteMap
里存放) - 否则看下有没有设置
@AliasFor
注解及value
值(别名)如果设置了,则判断是否是默认值(Method
可以获取方法默认值,也就是属性默认值),如果不是默认值,那么这个值就是被设置过的,此时就不应该覆盖它。 - 如果设置了
@AliasFor
注解同时不是默认值,那么就获取别名的值(注意这个别名的值也有可能是子注解传上来的,所以需要判断下overwriteMap
里有没有设置) - 最后将获得的最终的值存入克隆的
memberValues
中 - 然后处理要传到父注解的值,如果设置了
@AliasFor
的annotation
值,那么就将这个值设置到对应的overwriteMap
里,这样父注解在处理的时候就可以获取到这个值了
有了处理注解值的方法,那么就可以写遍历注解的方法了:
照样写个流程吧:
- 因为传入的是可被标注的元素,所以就取出这个元素所标注的所有注解,遍历
- 如果是 JDK 注解就直接跳过,因为这些注解对我们的程序无用,而且会导致无限递归
- 接着就是使用
AnnotatedElement
的getAnnotationsByType
方法获取标注在元素上的所有指定注解类型的值。之所以要这么做是因为 Java 支持重复注解,通过getAnnotations
的方法只能取得一个同类型的注解 - 取得注解后就调用
mergeAnnotationValue
方法合并注解值,同时把要传到父注解的值存入overwriteMap
中 - 除了 Java 标准的重复注解,我们还常用带 s 的注解包裹来做到重复注解,如
@MapperScans
和@MapperScan
此时就需要特殊处理,这里使用的方法是在带 s 的注解里添加一个@RepeatItem
注解,然后该注解存储单个注解的类型,这样就能把带 s 注解里的单个注解存到它对应类型的注解里。 - 处理完当前注解后就接着处理父注解(递归处理)
遍历完所有的注解后,就取得了可标注元素的所有注解处理过的 memberValues
,此时就可以将 memberValues
转成注解了。
此时就有了标注在可标注元素上所有的注解了,有了这些就可以封装成组合注解了:
首先是 MergedAnnotation
的接口:
然后就是实现类:
到这里我们自己的组合注解就实现完成了,让我们来使用下吧:
结语
组合注解简单化注解配置,用很少的注解来标注特定含义的多个元注解,同时提供了很好的扩展性,可以根据实际需要灵活的自定义注解。经过组合注解的重构后,我们就不再需要写很多注解处理器,避免了重复,同时也不易出错。