spring-core的注解工具提供的方法 AnnotatedElementUtils.findMergedRepeatableAnnotations
用于從AnnotatedElement 對象獲取可重復的注解。但如果注解本身也是可以定義在其他注解之上的元注解(meta-annotation),且該注解也是可重復注解。這個方法就可能會失效。這就是我最近在使用spring注解工具時遇到的問題。示例如下。
注解定義
先定義一組注解:
@Retention(RetentionPolicy.RUNTIME)@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD })@Repeatable(LevelAs.class)public @interface LevelA {String value();}@Retention(RetentionPolicy.RUNTIME)@Target({ ElementType.METHOD })public @interface LevelAs {LevelA[] value();}@Retention(RetentionPolicy.RUNTIME)@Target({ ElementType.ANNOTATION_TYPE,ElementType.METHOD })@LevelA("shadows")@Repeatable(LevelBs.class)public @interface LevelB {@AliasFor(annotation = LevelA.class,attribute = "value")String value();}@Retention(RetentionPolicy.RUNTIME)@Target({ ElementType.METHOD })public @interface LevelBs {LevelB[] value();}@Retention(RetentionPolicy.RUNTIME)@Target({ ElementType.METHOD })@LevelB("shadows")public @interface LevelC {@AliasFor(annotation = LevelB.class,attribute = "value")String value();}
上面定義了@LevalA,@LevalB,@LevalC
三個注解,其中@LeveA,@LevelB
是可以重復注解(參見 @LevelAs,@LevelBs
),而且還是元注解。@LeveA
是@LevelB
的元注解,@LevelB
是@LevelC
的元注解。它們三個層級關系是這樣的。
@LevelA 可重復 @LevelAs└─@LevelB 可重復 @LevelBs└─@LevelC
如下定義了測試這些注解的類
public static class TestClass{@LevelB("hello")@LevelB("world")@LevelA("top1")@LevelA("top2")@LevelC("jerry")public void foo() {};@LevelB("hello")@LevelA("top1")@LevelA("top2")@LevelC("jerry")public void tar() {};}
findMergedRepeatableAnnotations
如下調用AnnotatedElementUtils.findMergedRepeatableAnnotations
方法讀取TestClass.tar
上定義的所有@LevelA
注解
@Testpublic void test1() {try {Method method = TestClass.class.getMethod("tar");Set<LevelA> set = AnnotatedElementUtils.findMergedRepeatableAnnotations(method,LevelA.class);set.stream().forEach(a->{System.out.printf("%s\n", a);});} catch (Throwable e) {e.printStackTrace();fail();}}
確實可以獲取正確的結果:
@RepeatableAnnotTest$LevelA(value=top1)
@RepeatableAnnotTest$LevelA(value=top2)
@RepeatableAnnotTest$LevelA(value=hello)
@RepeatableAnnotTest$LevelA(value=jerry)
那是因為TestClass.tar
上只定義了一個@LevelB
注解,如果上面的測試代碼只是把方法名換為定義了多個@LevelB的方法foo
,結果就是這樣的:
@RepeatableAnnotTest$LevelA(value=top1)
@RepeatableAnnotTest$LevelA(value=top2)
@RepeatableAnnotTest$LevelA(value=jerry)
因為這里foo
方法定義了超過一個@LevelB
注解,實際定義在foo
上的注解就成了@LeveBs({@LevelB("hello"),@LevelB("world")})
,就導致AnnotatedElementUtils.findMergedRepeatableAnnotations
方法不能正確獲取。
分析AnnotatedElementUtils
的源碼,找到findMergedRepeatableAnnotations
方法實際的核心執行代碼如下:
RepeatableContainers repeatableContainers = RepeatableContainers.of(annotationType, containerType);
return MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY, repeatableContainers);
RepeatableContainers
進一查看RepeatableContainers的代碼發現它還有另一個方法standardRepeatables()
創建一個使用Java的@Repeatable注釋進行搜索的RepeatableContainers實例。
/*** Create a {@link RepeatableContainers} instance that searches using Java's* {@link Repeatable @Repeatable} annotation.* @return a {@link RepeatableContainers} instance*/public static RepeatableContainers standardRepeatables() {return StandardRepeatableContainers.INSTANCE;}
于是照著上面的代碼將測試代碼修改如下:
@Testpublic void test3() {try {Method method = TestClass.class.getMethod("foo");RepeatableContainers repeatableContainers = RepeatableContainers.standardRepeatables();MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY, repeatableContainers).stream(LevelA.class).forEach(a->{System.out.printf("%s\n", a.synthesize());});} catch (Throwable e) {e.printStackTrace();fail();}}
則結果正確:
@LevelA(value=top1)
@LevelA(value=top2)
@LevelA(value=hello)
@LevelA(value=world)
@LevelA(value=jerry)
完整測試代碼
import static org.junit.Assert.*;import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.Set;
import org.junit.Test;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.core.annotation.RepeatableContainers;public class RepeatableAnnotTest {@Testpublic void test1() {try {Method method = TestClass.class.getMethod("foo");Set<LevelA> set = AnnotatedElementUtils.findMergedRepeatableAnnotations(method,LevelA.class);set.stream().forEach(a->{System.out.printf("%s\n", a);});} catch (Throwable e) {e.printStackTrace();fail();}}@Testpublic void test2() {try {Method method = TestClass.class.getMethod("foo");RepeatableContainers repeatableContainers = RepeatableContainers.of(LevelA.class,null);MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY,repeatableContainers) .stream(LevelA.class) /* .stream() .sorted((o1,o2)->Integer.compare(o1.getDistance(), o2.getDistance()))*/.forEach(a->{System.out.printf("%s\n", a.synthesize());});} catch (Throwable e) {e.printStackTrace();fail();}}@Testpublic void test3() {try {Method method = TestClass.class.getMethod("foo");RepeatableContainers repeatableContainers = RepeatableContainers.standardRepeatables();MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY, repeatableContainers).stream(LevelA.class).forEach(a->{System.out.printf("%s\n", a.synthesize());});} catch (Throwable e) {e.printStackTrace();fail();}}@Retention(RetentionPolicy.RUNTIME)@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD })@Repeatable(LevelAs.class)public @interface LevelA {String value();}@Retention(RetentionPolicy.RUNTIME)@Target({ ElementType.METHOD })public @interface LevelAs {LevelA[] value();}@Retention(RetentionPolicy.RUNTIME)@Target({ ElementType.ANNOTATION_TYPE,ElementType.METHOD })@LevelA("shadows")@Repeatable(LevelBs.class)public @interface LevelB {@AliasFor(annotation = LevelA.class,attribute = "value")String value();}@Retention(RetentionPolicy.RUNTIME)@Target({ ElementType.METHOD })public @interface LevelBs {LevelB[] value();}@Retention(RetentionPolicy.RUNTIME)@Target({ ElementType.METHOD })@LevelB("shadows")public @interface LevelC {@AliasFor(annotation = LevelB.class,attribute = "value")String value();}public static class TestClass{@LevelB("hello")@LevelB("world")@LevelA("top1")@LevelA("top2")@LevelC("jerry")public void foo() {};@LevelB("hello")@LevelA("top1")@LevelA("top2")@LevelC("jerry")public void tar() {};}
}