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
1 2 3 4 |
@Retention(RetentionPolicy.RUNTIME) public @interface PropertySources { PropertySource[] value(); } |
然后使用时两个注解齐上阵
1 2 3 4 5 6 |
@PropertySources({ @PropertySource("classpath:config.properties"), @PropertySource("file:application.properties") }) public class MainApp { } |
看上去确实挺啰嗦的,用上了注解的嵌套形式。
该如何获得上面的注解内容呢?getAnnotation(PropertySources.class)
, 如
1 2 3 4 |
PropertySources annotation = MainApp.class.getAnnotation(PropertySources.class); for (PropertySource propertySource: annotation.value()){ System.out.println(propertySource.value()); } |
输出内容
classpath:config.properties
file:application.properties
Java 8 重复注解的使用改进
Java 8 看到了之前实现的繁琐之处,所以引入了一个注解的注解 @Repeatable
用来标识某个注解是可被重复使用的,但是同样需要一个容器注解。基于这里的例子,如果要实现可重复的注解必须要满足两个条件
- 前面的那个
@PropertySources
实现仍然是必须的,且实现是一样的,用以作为@PropertySource
的容器注解 - 用
@Repeatable(PropertySources.class)
注解@PropertySource
用以说明它的容器注解是@PropertySources
.
所以新的 @PropertySource
实现如下
1 2 3 4 5 |
@Retention(RetentionPolicy.RUNTIME) @Repeatable(PropertySources.class) //这行建立了 @PropertySource 与 @PropertySources 的关系 public @interface PropertySource { String value(); } |
@Repeatable
是 Java 8 开始提供的, 现在由它来告诉 Java @PropertySources
是 @PropertySource
的容器注解,而不需要用嵌套的方式来使用它们了,因此我们就能使用本文最初的注解形式,也就是
1 2 3 |
@PropertySource("classpath:config.properties") @PropertySource("file:application.properties") public class MainApp {} |
上面重复使用 @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")})
一样的内容实现,所以运行下上面与之前同样的反射代码
1 2 3 4 |
PropertySources annotation = MainApp.class.getAnnotation(PropertySources.class); for (PropertySource propertySource: annotation.value()){ System.out.println(propertySource.value()); } |
果不出所料,输出同样的内容
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 中对于
1 2 3 |
@PropertySource("classpath:config.properties") @PropertySource("file:application.properties") public class MainApp {} |
源程序中只是重复使用 @PropertySource
注解,然而却是要通过 @PropertySources
来反射得到 @PropertySource
的所有内容
1 2 3 4 |
PropertySources annotation = MainApp.class.getAnnotation(PropertySources.class); for (PropertySource propertySource: annotation.value()){ System.out.println(propertySource.value()); //获得所有的 @PropertySource 注解内容 } |
这里会很令人感到诧异的:明明注解时我们使用的是 @PropertySource
, 应该是用 getAnnotation(PropertySource.class)
来获得才对,可是
1 2 |
MainApp.class.getAnnotation(PropertySource.class); //得到的是 null MainApp.class.getAnnotations(); //得到的仍然是 @PropertySources |
通过前面的分析,为什么不能直接通过上面的 API 反射得到 @PropertySource
注解的原因当然我们已经知道了,内部实现使然。
所以 Java 8 为了避免在使用重复注解时的编码与反射时的尴尬,引入了一个新的反射注解的 API getAnnotationByType(Class<A> annotationClass)
, 该 API 的参数可接受 PropertySource.class
, 并且返回一个 @PropertySource
的数组。
最终 Java 8 推荐我们反射重复注解的途径就是下面那样
1 2 3 4 |
PropertySource[] propertySources = MainApp.class.getAnnotationsByType(PropertySource.class); for (PropertySource propertySource: propertySources ){ System.out.println(propertySource.value()); //获得所有 @PropertySource 的内容 } |
如果我们继续认真下去,窥探一下新的 getAnnotationsByType(Class<A> annotationClass)
在 Class.java 中的实现
1 2 3 4 5 6 7 8 |
public <A extends Annotation> A[] getAnnotationsByType(Class<A> annotationClass) { Objects.requireNonNull(annotationClass); AnnotationData annotationData = annotationData(); return AnnotationSupport.getAssociatedAnnotations(annotationData.declaredAnnotations, this, annotationClass); } |
解释 MainApp.class.getAnnotationsByType(PropertySource.class)
的执行过程:
- 如果能找到
@Repeatable
关联的容器注解类@PropertySources
, 就获得@PropertySources
的所有 value(类型为@PropertySource
) 值组成的数组 - 如果未有关联的容器注解类,则返回
@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
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
清晰细致,点赞
实在不错,特别是可重复注解的描述