Java 8 可重复注解的理解与应用

Java 8 之前如何重复使用注解

在 Java 8 之前我们不能在一个类型重复使用同一个注解,例如 Spring 的注解 @PropertySource 不能下面那样来引入多个属性文件

@PropertySource("classpath:config.properties")
@PropertySource("file:application.properties")
public class MainApp {}

上面的代码无法在 Java 7 下通过编译,错误是: Duplicate annotation

于是我们在 Java 8 之前想到了一个方案来规避 Duplicate Annotation 的错误: 即声明一个新的 Annotation 来包裹 @PropertySource, 如 @PropertySources

然后使用时两个注解齐上阵

看上去确实挺啰嗦的,用上了注解的嵌套形式。

该如何获得上面的注解内容呢?getAnnotation(PropertySources.class), 如

输出内容

classpath:config.properties
file:application.properties

Java 8 重复注解的使用改进

Java 8 看到了之前实现的繁琐之处,所以引入了一个注解的注解 @Repeatable 用来标识某个注解是可被重复使用的,但是同样需要一个容器注解。基于这里的例子,如果要实现可重复的注解必须要满足两个条件

  1. 前面的那个 @PropertySources 实现仍然是必须的,且实现是一样的,用以作为 @PropertySource 的容器注解
  2. @Repeatable(PropertySources.class) 注解 @PropertySource 用以说明它的容器注解是 @PropertySources.

所以新的 @PropertySource 实现如下

@Repeatable 是 Java 8 开始提供的, 现在由它来告诉 Java @PropertySources 是 @PropertySource 的容器注解,而不需要用嵌套的方式来使用它们了,因此我们就能使用本文最初的注解形式,也就是

上面重复使用 @PropertySource 注解在 Java 8 下是合法的,并且实际效果完全等同于 Java 8 之前嵌套的方式。区别就是显式的写出嵌套关系,还是由 @Repeatable 来建立隐含的嵌套关系。

注意,如果 @PropertySource 没有用  @Repeatable 注解而重复使用的话,会报出错误:

Duplicate annotation. The declaration of 'PropertySource' does not have a valid java.lang.annotation.Repeatable annotation

重复注解准备好了,现在来看使用中没有出现 @PropertySources 的情况下该如何反射得到注解内容呢?

基于前面对 Java 8 前后对重复注解实现的对比,猜想着 @Repeatable(PropertySources.class) 的 @PropertySource 应该会转换成 @PropertySources({@PropertySource("xxx")}) 一样的内容实现,所以运行下上面与之前同样的反射代码

果不出所料,输出同样的内容

classpath:config.properties
file:application.properties

实际上在字节码中 Java 8 前后对重复注解的内部实现也确实是一样的,@Repeatable 还真就是个语法糖而已。对于在 Java 8 下重复使用 @PropertySource 注解的 MainApp 类我们用 javap -v 来查看生成的字节码,得到如下的结果

➜ classes javap -v cc.unmi.MainApp
Classfile /Users/Yanbin/Workspaces/github/test_repeatable_annotation/target/classes/cc/unmi/MainApp.class
Last modified Jan 16, 2017; size 450 bytes
MD5 checksum 6727710e42775e09539f1575d347f6e0
Compiled from "MainApp.java"
public class cc.unmi.MainApp
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#19 // java/lang/Object."<init>":()V
#2 = Class #20 // cc/unmi/MainApp
#3 = Class #21 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 Lcc/unmi/MainApp;
#11 = Utf8 SourceFile
#12 = Utf8 MainApp.java
#13 = Utf8 RuntimeVisibleAnnotations
#14 = Utf8 Lcc/unmi/PropertySources;
#15 = Utf8 value
#16 = Utf8 Lcc/unmi/PropertySource;
#17 = Utf8 classpath:config.properties
#18 = Utf8 file:application.properties
#19 = NameAndType #4:#5 // "<init>":()V
#20 = Utf8 cc/unmi/MainApp
#21 = Utf8 java/lang/Object
{
public cc.unmi.MainApp();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcc/unmi/MainApp;
}
SourceFile: "MainApp.java"
RuntimeVisibleAnnotations:
0: #14(#15=[@#16(#15=s#17),@#16(#15=s#18)])

最后两行显示了保留在运行时的注解,并且只有一个,内容是

#14(#15=[@#16(#15=s#17),@#16(#15=s#18)])

按编号对照前面的常量池定义,上面注解可翻译为

@PropertySources(value=[@PropertySource(value="classpath:config.properties"), @PropertySource(value="file:application.properties")])

这实际上与 Java 8 之前用 @PropertySources 与 @PropertySource 嵌套的写法是完全一致的,只是在字节码描述中注解的数组值是用是用  [] 来表示。

Java 8 下如何反射获得重复注解的内容

这个问题在前一节中回答了一部分,依然可通过容器注解像 Java 8 之前一样的办法反射得到重复注解的内容。虽然刚在上一节中演示了,但还是再温习一下,在 Java 8 中对于

源程序中只是重复使用 @PropertySource 注解,然而却是要通过 @PropertySources 来反射得到 @PropertySource 的所有内容

这里会很令人感到诧异的:明明注解时我们使用的是 @PropertySource, 应该是用 getAnnotation(PropertySource.class) 来获得才对,可是

通过前面的分析,为什么不能直接通过上面的 API 反射得到  @PropertySource 注解的原因当然我们已经知道了,内部实现使然。

所以 Java 8 为了避免在使用重复注解时的编码与反射时的尴尬,引入了一个新的反射注解的 API getAnnotationByType(Class<A> annotationClass), 该 API 的参数可接受 PropertySource.class, 并且返回一个 @PropertySource 的数组。

最终 Java 8 推荐我们反射重复注解的途径就是下面那样

如果我们继续认真下去,窥探一下新的 getAnnotationsByType(Class<A> annotationClass) 在 Class.java 中的实现

解释 MainApp.class.getAnnotationsByType(PropertySource.class) 的执行过程:

  1. 如果能找到 @Repeatable 关联的容器注解类 @PropertySources, 就获得 @PropertySources 的所有 value(类型为 @PropertySource) 值组成的数组
  2. 如果未有关联的容器注解类,则返回 @PropertySource 本身组成的数组(只有一个元素), 此时和 new PropertySource[]{MainApp.class.getAnnotation(PropertySource.class} 一样的。

因此,在 Java 8 中对于可重复注解应该调用 getAnnotationsByType(Class<A> annotationClass) 来反射得到,如果是不可重复注解建议还是调用原来的 getAnnotation(Class<A> annotationClass), 因为没必要使用 getAnnotationsByType(...) 获得一个空的或 1 个元素的数组。

本文链接 https://yanbin.blog/java8-repeatable-annotations/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

2 Comments
Inline Feedbacks
View all comments
我不会秃的
我不会秃的
3 years ago

清晰细致,点赞

satopendragon
satopendragon
4 years ago

实在不错,特别是可重复注解的描述