Skip to content

Commit a5139f3

Browse files
committed
Support searches for multiple merged composed annotations
Prior to this commit it was possible to search for the 1st merged annotation above an annotated element. It was also possible to search for annotation attributes aggregated from all annotations above an annotated element; however, it was impossible to search for all composed annotations above an annotated element and have the results as synthesized merged annotations instead of a multi-map of attributes. This commit introduces a new findAllMergedAnnotations() method in AnnotatedElementUtils that finds all annotations of the specified type within the annotation hierarchy above the supplied element. For each such annotation found, it merges that annotation's attributes with matching attributes from annotations in lower levels of the annotation hierarchy and synthesizes the results back into an annotation of the specified type. All such merged annotations are collected and returned as a set. Issue: SPR-13486
1 parent 799736c commit a5139f3

File tree

2 files changed

+237
-30
lines changed

2 files changed

+237
-30
lines changed

spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java

Lines changed: 120 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ public static AnnotationAttributes getMergedAnnotationAttributes(AnnotatedElemen
424424
* @param annotationType the annotation type to find
425425
* @return the merged, synthesized {@code Annotation}, or {@code null} if not found
426426
* @since 4.2
427+
* @see #findAllMergedAnnotations(AnnotatedElement, Class)
427428
* @see #findMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean)
428429
* @see #getMergedAnnotationAttributes(AnnotatedElement, Class)
429430
*/
@@ -460,6 +461,41 @@ public static <A extends Annotation> A findMergedAnnotation(AnnotatedElement ele
460461
return AnnotationUtils.synthesizeAnnotation(attributes, (Class<A>) attributes.annotationType(), element);
461462
}
462463

464+
/**
465+
* Find <strong>all</strong> annotations of the specified {@code annotationType}
466+
* within the annotation hierarchy <em>above</em> the supplied {@code element};
467+
* and for each annotation found, merge that annotation's attributes with
468+
* <em>matching</em> attributes from annotations in lower levels of the annotation
469+
* hierarchy, and synthesize the result back into an annotation of the specified
470+
* {@code annotationType}.
471+
* <p>{@link AliasFor @AliasFor} semantics are fully supported, both within a
472+
* single annotation and within the annotation hierarchy.
473+
* @param element the annotated element; never {@code null}
474+
* @param annotationType the annotation type to find; never {@code null}
475+
* @return the set of all merged, synthesized {@code Annotations} found, or an empty
476+
* set if none were found
477+
* @since 4.3
478+
* @see #findMergedAnnotation(AnnotatedElement, Class)
479+
*/
480+
public static <A extends Annotation> Set<A> findAllMergedAnnotations(AnnotatedElement element,
481+
Class<A> annotationType) {
482+
483+
Assert.notNull(element, "AnnotatedElement must not be null");
484+
Assert.notNull(annotationType, "annotationType must not be null");
485+
486+
MergedAnnotationAttributesProcessor processor = new MergedAnnotationAttributesProcessor(annotationType, null,
487+
false, false, true);
488+
489+
searchWithFindSemantics(element, annotationType, annotationType.getName(), processor);
490+
491+
Set<A> annotations = new LinkedHashSet<A>();
492+
for (AnnotationAttributes attributes : processor.getAggregatedResults()) {
493+
AnnotationUtils.postProcessAnnotationAttributes(element, attributes, false, false);
494+
annotations.add(AnnotationUtils.synthesizeAnnotation(attributes, annotationType, element));
495+
}
496+
return annotations;
497+
}
498+
463499
/**
464500
* Find the first annotation of the specified {@code annotationType} within
465501
* the annotation hierarchy <em>above</em> the supplied {@code element} and
@@ -796,6 +832,8 @@ private static <T> T searchWithFindSemantics(AnnotatedElement element, Class<? e
796832
// Locally declared annotations (ignoring @Inherited)
797833
Annotation[] annotations = element.getDeclaredAnnotations();
798834

835+
List<T> aggregatedResults = processor.aggregates() ? new ArrayList<T>() : null;
836+
799837
// Search in local annotations
800838
for (Annotation annotation : annotations) {
801839
if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation) &&
@@ -804,7 +842,12 @@ private static <T> T searchWithFindSemantics(AnnotatedElement element, Class<? e
804842
metaDepth > 0)) {
805843
T result = processor.process(element, annotation, metaDepth);
806844
if (result != null) {
807-
return result;
845+
if (processor.aggregates() && metaDepth == 0) {
846+
aggregatedResults.add(result);
847+
}
848+
else {
849+
return result;
850+
}
808851
}
809852
}
810853
}
@@ -816,11 +859,20 @@ private static <T> T searchWithFindSemantics(AnnotatedElement element, Class<? e
816859
annotation.annotationType(), annotationType, annotationName, processor, visited, metaDepth + 1);
817860
if (result != null) {
818861
processor.postProcess(annotation.annotationType(), annotation, result);
819-
return result;
862+
if (processor.aggregates() && metaDepth == 0) {
863+
aggregatedResults.add(result);
864+
}
865+
else {
866+
return result;
867+
}
820868
}
821869
}
822870
}
823871

872+
if (processor.aggregates()) {
873+
processor.getAggregatedResults().addAll(0, aggregatedResults);
874+
}
875+
824876
if (element instanceof Method) {
825877
Method method = (Method) element;
826878

@@ -930,11 +982,16 @@ private static <T> T searchOnInterfaces(Method method, Class<? extends Annotatio
930982
* annotations, or all annotations discovered by the currently executing
931983
* search. The term "target" in this context refers to a matching
932984
* annotation (i.e., a specific annotation type that was found during
933-
* the search). Returning a non-null value from the {@link #process}
985+
* the search).
986+
* <p>Returning a non-null value from the {@link #process}
934987
* method instructs the search algorithm to stop searching further;
935988
* whereas, returning {@code null} from the {@link #process} method
936989
* instructs the search algorithm to continue searching for additional
937-
* annotations.
990+
* annotations. One exception to this rule applies to processors
991+
* that {@linkplain #aggregates aggregate} results. If an aggregating
992+
* processor returns a non-null value, that value will be added to the
993+
* list of {@linkplain #getAggregatedResults aggregated results}
994+
* and the search algorithm will continue.
938995
* <p>Processors can optionally {@linkplain #postProcess post-process}
939996
* the result of the {@link #process} method as the search algorithm
940997
* goes back down the annotation hierarchy from an invocation of
@@ -983,12 +1040,38 @@ private interface Processor<T> {
9831040
* @param result the result to post-process
9841041
*/
9851042
void postProcess(AnnotatedElement annotatedElement, Annotation annotation, T result);
986-
}
9871043

1044+
/**
1045+
* Determine if this processor aggregates the results returned by {@link #process}.
1046+
* <p>If this method returns {@code true}, then {@link #getAggregatedResults()}
1047+
* must return a non-null value.
1048+
* <p>WARNING: aggregation is currently only supported for <em>find semantics</em>.
1049+
* @return {@code true} if this processor supports aggregated results
1050+
* @see #getAggregatedResults
1051+
* @since 4.3
1052+
*/
1053+
boolean aggregates();
1054+
1055+
/**
1056+
* Get the list of results aggregated by this processor.
1057+
* <p>NOTE: the processor does not aggregate the results itself.
1058+
* Rather, the search algorithm that uses this processor is responsible
1059+
* for asking this processor if it {@link #aggregates} results and then
1060+
* adding the post-processed results to the list returned by this
1061+
* method.
1062+
* <p>WARNING: aggregation is currently only supported for <em>find semantics</em>.
1063+
* @return the list of results aggregated by this processor; never
1064+
* {@code null} unless {@link #aggregates} returns {@code false}
1065+
* @see #aggregates
1066+
* @since 4.3
1067+
*/
1068+
List<T> getAggregatedResults();
1069+
}
9881070

9891071
/**
990-
* {@link Processor} that {@linkplain #process processes} annotations
991-
* but does not {@linkplain #postProcess post-process} results.
1072+
* {@link Processor} that {@linkplain #process(AnnotatedElement, Annotation, int)
1073+
* processes} annotations but does not {@linkplain #postProcess post-process} or
1074+
* {@linkplain #aggregates aggregate} results.
9921075
* @since 4.2
9931076
*/
9941077
private abstract static class SimpleAnnotationProcessor<T> implements Processor<T> {
@@ -997,6 +1080,16 @@ private abstract static class SimpleAnnotationProcessor<T> implements Processor<
9971080
public final void postProcess(AnnotatedElement annotatedElement, Annotation annotation, T result) {
9981081
// no-op
9991082
}
1083+
1084+
@Override
1085+
public final boolean aggregates() {
1086+
return false;
1087+
}
1088+
1089+
@Override
1090+
public List<T> getAggregatedResults() {
1091+
throw new UnsupportedOperationException("SimpleAnnotationProcessor does not support aggregated results");
1092+
}
10001093
}
10011094

10021095

@@ -1019,13 +1112,33 @@ private static class MergedAnnotationAttributesProcessor implements Processor<An
10191112

10201113
private final boolean nestedAnnotationsAsMap;
10211114

1115+
private final List<AnnotationAttributes> aggregatedResults;
1116+
1117+
10221118
MergedAnnotationAttributesProcessor(Class<? extends Annotation> annotationType, String annotationName,
10231119
boolean classValuesAsString, boolean nestedAnnotationsAsMap) {
10241120

1121+
this(annotationType, annotationName, classValuesAsString, nestedAnnotationsAsMap, false);
1122+
}
1123+
1124+
MergedAnnotationAttributesProcessor(Class<? extends Annotation> annotationType, String annotationName,
1125+
boolean classValuesAsString, boolean nestedAnnotationsAsMap, boolean aggregates) {
1126+
10251127
this.annotationType = annotationType;
10261128
this.annotationName = annotationName;
10271129
this.classValuesAsString = classValuesAsString;
10281130
this.nestedAnnotationsAsMap = nestedAnnotationsAsMap;
1131+
this.aggregatedResults = (aggregates ? new ArrayList<AnnotationAttributes>() : null);
1132+
}
1133+
1134+
@Override
1135+
public boolean aggregates() {
1136+
return this.aggregatedResults != null;
1137+
}
1138+
1139+
@Override
1140+
public List<AnnotationAttributes> getAggregatedResults() {
1141+
return this.aggregatedResults;
10291142
}
10301143

10311144
@Override

spring-core/src/test/java/org/springframework/core/annotation/MultipleComposedAnnotationsOnSingleAnnotatedElementTests.java

Lines changed: 117 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
import java.lang.annotation.RetentionPolicy;
2323
import java.lang.annotation.Target;
2424
import java.lang.reflect.AnnotatedElement;
25+
import java.lang.reflect.Method;
26+
import java.util.Iterator;
27+
import java.util.Set;
2528

2629
import org.junit.Test;
2730

@@ -43,36 +46,76 @@ public class MultipleComposedAnnotationsOnSingleAnnotatedElementTests {
4346

4447
@Test
4548
public void multipleComposedAnnotationsOnClass() {
46-
assertMultipleComposedAnnotations(MultipleCachesClass.class);
49+
assertMultipleComposedAnnotations(MultipleComposedCachesClass.class);
50+
}
51+
52+
@Test
53+
public void composedPlusLocalAnnotationsOnClass() {
54+
assertMultipleComposedAnnotations(ComposedPlusLocalCachesClass.class);
55+
}
56+
57+
@Test
58+
public void multipleComposedAnnotationsOnInterface() {
59+
assertMultipleComposedAnnotations(MultipleComposedCachesOnInterfaceClass.class);
60+
}
61+
62+
@Test
63+
public void composedCacheOnInterfaceAndLocalCacheOnClass() {
64+
assertMultipleComposedAnnotations(ComposedCacheOnInterfaceAndLocalCacheClass.class);
4765
}
4866

4967
@Test
5068
public void multipleComposedAnnotationsOnMethod() throws Exception {
51-
AnnotatedElement element = getClass().getDeclaredMethod("multipleCachesMethod");
69+
AnnotatedElement element = getClass().getDeclaredMethod("multipleComposedCachesMethod");
5270
assertMultipleComposedAnnotations(element);
5371
}
5472

55-
private void assertMultipleComposedAnnotations(AnnotatedElement element) {
56-
assertNotNull(element);
57-
58-
// Prerequisites
59-
FooCache fooCache = element.getAnnotation(FooCache.class);
60-
BarCache barCache = element.getAnnotation(BarCache.class);
61-
assertNotNull(fooCache);
62-
assertNotNull(barCache);
63-
assertEquals("fooKey", fooCache.key());
64-
assertEquals("barKey", barCache.key());
73+
@Test
74+
public void composedPlusLocalAnnotationsOnMethod() throws Exception {
75+
AnnotatedElement element = getClass().getDeclaredMethod("composedPlusLocalCachesMethod");
76+
assertMultipleComposedAnnotations(element);
77+
}
6578

66-
// Assert the status quo for finding the 1st merged annotation.
67-
Cacheable cacheable = findMergedAnnotation(element, Cacheable.class);
68-
assertNotNull(cacheable);
69-
assertEquals("fooCache", cacheable.value());
70-
assertEquals("fooKey", cacheable.key());
79+
/**
80+
* Bridge/bridged method setup code copied from
81+
* {@link org.springframework.core.BridgeMethodResolverTests#testWithGenericParameter()}.
82+
*/
83+
@Test
84+
public void multipleComposedAnnotationsBridgeMethod() throws NoSuchMethodException {
85+
Method[] methods = StringGenericParameter.class.getMethods();
86+
Method bridgeMethod = null;
87+
Method bridgedMethod = null;
88+
89+
for (Method method : methods) {
90+
if ("getFor".equals(method.getName()) && !method.getParameterTypes()[0].equals(Integer.class)) {
91+
if (method.getReturnType().equals(Object.class)) {
92+
bridgeMethod = method;
93+
}
94+
else {
95+
bridgedMethod = method;
96+
}
97+
}
98+
}
99+
assertTrue(bridgeMethod != null && bridgeMethod.isBridge());
100+
assertTrue(bridgedMethod != null && !bridgedMethod.isBridge());
101+
102+
assertMultipleComposedAnnotations(bridgeMethod);
103+
}
71104

72-
// TODO Introduce findMergedAnnotations(...) in AnnotatedElementUtils.
105+
private void assertMultipleComposedAnnotations(AnnotatedElement element) {
106+
assertNotNull(element);
73107

74-
// assertEquals("barCache", cacheable.value());
75-
// assertEquals("barKey", cacheable.key());
108+
Set<Cacheable> cacheables = findAllMergedAnnotations(element, Cacheable.class);
109+
assertNotNull(cacheables);
110+
assertEquals(2, cacheables.size());
111+
112+
Iterator<Cacheable> iterator = cacheables.iterator();
113+
Cacheable fooCacheable = iterator.next();
114+
Cacheable barCacheable = iterator.next();
115+
assertEquals("fooKey", fooCacheable.key());
116+
assertEquals("fooCache", fooCacheable.value());
117+
assertEquals("barKey", barCacheable.key());
118+
assertEquals("barCache", barCacheable.value());
76119
}
77120

78121

@@ -86,7 +129,11 @@ private void assertMultipleComposedAnnotations(AnnotatedElement element) {
86129
@Inherited
87130
@interface Cacheable {
88131

89-
String value();
132+
@AliasFor("cacheName")
133+
String value() default "";
134+
135+
@AliasFor("value")
136+
String cacheName() default "";
90137

91138
String key() default "";
92139
}
@@ -113,13 +160,60 @@ private void assertMultipleComposedAnnotations(AnnotatedElement element) {
113160

114161
@FooCache(key = "fooKey")
115162
@BarCache(key = "barKey")
116-
private static class MultipleCachesClass {
163+
private static class MultipleComposedCachesClass {
117164
}
118165

166+
@Cacheable(cacheName = "fooCache", key = "fooKey")
167+
@BarCache(key = "barKey")
168+
private static class ComposedPlusLocalCachesClass {
169+
}
119170

120171
@FooCache(key = "fooKey")
121172
@BarCache(key = "barKey")
122-
private void multipleCachesMethod() {
173+
private interface MultipleComposedCachesInterface {
174+
}
175+
176+
private static class MultipleComposedCachesOnInterfaceClass implements MultipleComposedCachesInterface {
177+
}
178+
179+
@Cacheable(cacheName = "fooCache", key = "fooKey")
180+
private interface ComposedCacheInterface {
181+
}
182+
183+
@BarCache(key = "barKey")
184+
private static class ComposedCacheOnInterfaceAndLocalCacheClass implements ComposedCacheInterface {
185+
}
186+
187+
188+
@FooCache(key = "fooKey")
189+
@BarCache(key = "barKey")
190+
private void multipleComposedCachesMethod() {
191+
}
192+
193+
@Cacheable(cacheName = "fooCache", key = "fooKey")
194+
@BarCache(key = "barKey")
195+
private void composedPlusLocalCachesMethod() {
196+
}
197+
198+
199+
public interface GenericParameter<T> {
200+
201+
T getFor(Class<T> cls);
202+
}
203+
204+
@SuppressWarnings("unused")
205+
private static class StringGenericParameter implements GenericParameter<String> {
206+
207+
@FooCache(key = "fooKey")
208+
@BarCache(key = "barKey")
209+
@Override
210+
public String getFor(Class<String> cls) {
211+
return "foo";
212+
}
213+
214+
public String getFor(Integer integer) {
215+
return "foo";
216+
}
123217
}
124218

125219
}

0 commit comments

Comments
 (0)