Skip to content

Commit b9b0b78

Browse files
committed
Support n meta-annotation levels on methods in AnnotationUtils
Prior to this commit, the search algorithm used by the findAnnotation(Method, Class) method in AnnotationUtils only found direct annotations or direct meta-annotations (i.e., one level of meta-annotations). This commit reworks the search algorithm so that it supports arbitrary levels of meta-annotations on methods. To make this possible, a new findAnnotation(AnnotatedElement, Class) method has been introduced in AnnotationUtils. This fix also allows for the @ignore'd tests in TransactionalEventListenerTests to be re-enabled. Issue: SPR-12941
1 parent 666d1ce commit b9b0b78

File tree

3 files changed

+140
-29
lines changed

3 files changed

+140
-29
lines changed

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

Lines changed: 93 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,9 @@ public abstract class AnnotationUtils {
9191
* Get a single {@link Annotation} of {@code annotationType} from the supplied
9292
* annotation: either the given annotation itself or a direct meta-annotation
9393
* thereof.
94-
* <p>Note that this method does <em>not</em> support arbitrary levels of
95-
* meta-annotations.
94+
* <p>Note that this method supports only a single level of meta-annotations.
95+
* For support for arbitrary levels of meta-annotations, use one of the
96+
* {@code find*()} methods instead.
9697
* @param ann the Annotation to check
9798
* @param annotationType the annotation type to look for, both locally and as a meta-annotation
9899
* @return the matching annotation, or {@code null} if not found
@@ -115,9 +116,11 @@ public static <T extends Annotation> T getAnnotation(Annotation ann, Class<T> an
115116

116117
/**
117118
* Get a single {@link Annotation} of {@code annotationType} from the supplied
118-
* {@link AnnotatedElement}.
119-
* <p>Meta-annotations will be searched if the annotation is not
120-
* <em>directly present</em> on the supplied element.
119+
* {@link AnnotatedElement}, where the {@code AnnotatedElement} is either
120+
* directly annotated or meta-annotated with the {@code annotationType}.
121+
* <p>Note that this method supports only a single level of meta-annotations.
122+
* For support for arbitrary levels of meta-annotations, use
123+
* {@link #findAnnotation(AnnotatedElement, Class)} instead.
121124
* @param annotatedElement the {@code AnnotatedElement} from which to get the annotation
122125
* @param annotationType the annotation type to look for, both locally and as a meta-annotation
123126
* @return the matching annotation, or {@code null} if not found
@@ -144,10 +147,13 @@ public static <T extends Annotation> T getAnnotation(AnnotatedElement annotatedE
144147
}
145148

146149
/**
147-
* Get a single {@link Annotation} of {@code annotationType} from the supplied {@link Method}.
150+
* Get a single {@link Annotation} of {@code annotationType} from the
151+
* supplied {@link Method}, where the method is either directly annotated
152+
* or meta-annotated with the {@code annotationType}.
148153
* <p>Correctly handles bridge {@link Method Methods} generated by the compiler.
149-
* <p>Meta-annotations will be searched if the annotation is not
150-
* <em>directly present</em> on the supplied method.
154+
* <p>Note that this method supports only a single level of meta-annotations.
155+
* For support for arbitrary levels of meta-annotations, use
156+
* {@link #findAnnotation(Method, Class)} instead.
151157
* @param method the method to look for annotations on
152158
* @param annotationType the annotation type to look for
153159
* @return the matching annotation, or {@code null} if not found
@@ -256,34 +262,100 @@ public static <A extends Annotation> Set<A> getRepeatableAnnotation(AnnotatedEle
256262
}
257263

258264
/**
259-
* Find a single {@link Annotation} of {@code annotationType} from the supplied
265+
* Find a single {@link Annotation} of {@code annotationType} on the
266+
* supplied {@link AnnotatedElement}.
267+
* <p>Meta-annotations will be searched if the annotation is not
268+
* <em>directly present</em> on the supplied element.
269+
* <p><strong>Warning</strong>: this method operates generically on
270+
* annotated elements. In other words, this method does not execute
271+
* specialized search algorithms for classes or methods. If you require
272+
* the more specific semantics of {@link #findAnnotation(Class, Class)}
273+
* or {@link #findAnnotation(Method, Class)}, invoke one of those methods
274+
* instead.
275+
* @param annotatedElement the {@code AnnotatedElement} on which to find the annotation
276+
* @param annotationType the annotation type to look for, both locally and as a meta-annotation
277+
* @return the matching annotation, or {@code null} if not found
278+
* @since 4.2
279+
*/
280+
public static <A extends Annotation> A findAnnotation(AnnotatedElement annotatedElement, Class<A> annotationType) {
281+
// Do NOT store result in the findAnnotationCache since doing so could break
282+
// findAnnotation(Class, Class) and findAnnotation(Method, Class).
283+
return findAnnotation(annotatedElement, annotationType, new HashSet<Annotation>());
284+
}
285+
286+
/**
287+
* Perform the search algorithm for {@link #findAnnotation(AnnotatedElement, Class)}
288+
* avoiding endless recursion by tracking which annotations have already
289+
* been <em>visited</em>.
290+
* @param annotatedElement the {@code AnnotatedElement} on which to find the annotation
291+
* @param annotationType the annotation type to look for, both locally and as a meta-annotation
292+
* @param visited the set of annotations that have already been visited
293+
* @return the matching annotation, or {@code null} if not found
294+
* @since 4.2
295+
*/
296+
@SuppressWarnings("unchecked")
297+
private static <T extends Annotation> T findAnnotation(AnnotatedElement annotatedElement, Class<T> annotationType, Set<Annotation> visited) {
298+
Assert.notNull(annotatedElement, "AnnotatedElement must not be null");
299+
try {
300+
Annotation[] anns = annotatedElement.getDeclaredAnnotations();
301+
for (Annotation ann : anns) {
302+
if (ann.annotationType().equals(annotationType)) {
303+
return (T) ann;
304+
}
305+
}
306+
for (Annotation ann : anns) {
307+
if (!isInJavaLangAnnotationPackage(ann) && visited.add(ann)) {
308+
T annotation = findAnnotation((AnnotatedElement) ann.annotationType(), annotationType, visited);
309+
if (annotation != null) {
310+
return annotation;
311+
}
312+
}
313+
}
314+
}
315+
catch (Exception ex) {
316+
// Assuming nested Class values not resolvable within annotation attributes...
317+
logIntrospectionFailure(annotatedElement, ex);
318+
}
319+
return null;
320+
}
321+
322+
/**
323+
* Find a single {@link Annotation} of {@code annotationType} on the supplied
260324
* {@link Method}, traversing its super methods (i.e., from superclasses and
261325
* interfaces) if no annotation can be found on the given method itself.
326+
* <p>Correctly handles bridge {@link Method Methods} generated by the compiler.
327+
* <p>Meta-annotations will be searched if the annotation is not
328+
* <em>directly present</em> on the method.
262329
* <p>Annotations on methods are not inherited by default, so we need to handle
263330
* this explicitly.
264-
* <p>Meta-annotations will <em>not</em> be searched.
265331
* @param method the method to look for annotations on
266332
* @param annotationType the annotation type to look for
267333
* @return the matching annotation, or {@code null} if not found
334+
* @see #getAnnotation(Method, Class)
268335
*/
269336
@SuppressWarnings("unchecked")
270337
public static <A extends Annotation> A findAnnotation(Method method, Class<A> annotationType) {
271338
AnnotationCacheKey cacheKey = new AnnotationCacheKey(method, annotationType);
272339
A result = (A) findAnnotationCache.get(cacheKey);
340+
273341
if (result == null) {
274-
result = getAnnotation(method, annotationType);
275-
Class<?> clazz = method.getDeclaringClass();
342+
Method resolvedMethod = BridgeMethodResolver.findBridgedMethod(method);
343+
result = findAnnotation((AnnotatedElement) resolvedMethod, annotationType);
344+
276345
if (result == null) {
277-
result = searchOnInterfaces(method, annotationType, clazz.getInterfaces());
346+
result = searchOnInterfaces(method, annotationType, method.getDeclaringClass().getInterfaces());
278347
}
348+
349+
Class<?> clazz = method.getDeclaringClass();
279350
while (result == null) {
280351
clazz = clazz.getSuperclass();
281352
if (clazz == null || clazz.equals(Object.class)) {
282353
break;
283354
}
284355
try {
285356
Method equivalentMethod = clazz.getDeclaredMethod(method.getName(), method.getParameterTypes());
286-
result = getAnnotation(equivalentMethod, annotationType);
357+
Method resolvedEquivalentMethod = BridgeMethodResolver.findBridgedMethod(equivalentMethod);
358+
result = findAnnotation((AnnotatedElement) resolvedEquivalentMethod, annotationType);
287359
}
288360
catch (NoSuchMethodException ex) {
289361
// No equivalent method found
@@ -292,9 +364,10 @@ public static <A extends Annotation> A findAnnotation(Method method, Class<A> an
292364
result = searchOnInterfaces(method, annotationType, clazz.getInterfaces());
293365
}
294366
}
295-
if (result != null) {
296-
findAnnotationCache.put(cacheKey, result);
297-
}
367+
}
368+
369+
if (result != null) {
370+
findAnnotationCache.put(cacheKey, result);
298371
}
299372
return result;
300373
}
@@ -680,7 +753,7 @@ else if (nestedAnnotationsAsMap && value instanceof Annotation[]) {
680753
}
681754

682755
/**
683-
* Retrieve the <em>value</em> of the {@code &quot;value&quot;} attribute of a
756+
* Retrieve the <em>value</em> of the {@code value} attribute of a
684757
* single-element Annotation, given an annotation instance.
685758
* @param annotation the annotation instance from which to retrieve the value
686759
* @return the attribute value, or {@code null} if not found
@@ -712,7 +785,7 @@ public static Object getValue(Annotation annotation, String attributeName) {
712785
}
713786

714787
/**
715-
* Retrieve the <em>default value</em> of the {@code &quot;value&quot;} attribute
788+
* Retrieve the <em>default value</em> of the {@code value} attribute
716789
* of a single-element Annotation, given an annotation instance.
717790
* @param annotation the annotation instance from which to retrieve the default value
718791
* @return the default value, or {@code null} if not found
@@ -737,7 +810,7 @@ public static Object getDefaultValue(Annotation annotation, String attributeName
737810
}
738811

739812
/**
740-
* Retrieve the <em>default value</em> of the {@code &quot;value&quot;} attribute
813+
* Retrieve the <em>default value</em> of the {@code value} attribute
741814
* of a single-element Annotation, given the {@link Class annotation type}.
742815
* @param annotationType the <em>annotation type</em> for which the default value should be retrieved
743816
* @return the default value, or {@code null} if not found

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

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,31 +50,55 @@ public class AnnotationUtilsTests {
5050

5151
@Test
5252
public void findMethodAnnotationOnLeaf() throws Exception {
53-
Method m = Leaf.class.getMethod("annotatedOnLeaf", (Class[]) null);
53+
Method m = Leaf.class.getMethod("annotatedOnLeaf");
5454
assertNotNull(m.getAnnotation(Order.class));
5555
assertNotNull(getAnnotation(m, Order.class));
5656
assertNotNull(findAnnotation(m, Order.class));
5757
}
5858

59+
@Test
60+
public void findMethodAnnotationWithMetaAnnotationOnLeaf() throws Exception {
61+
Method m = Leaf.class.getMethod("metaAnnotatedOnLeaf");
62+
assertNull(m.getAnnotation(Order.class));
63+
assertNotNull(getAnnotation(m, Order.class));
64+
assertNotNull(findAnnotation(m, Order.class));
65+
}
66+
67+
@Test
68+
public void findMethodAnnotationWithMetaMetaAnnotationOnLeaf() throws Exception {
69+
Method m = Leaf.class.getMethod("metaMetaAnnotatedOnLeaf");
70+
assertNull(m.getAnnotation(Component.class));
71+
assertNull(getAnnotation(m, Component.class));
72+
assertNotNull(findAnnotation(m, Component.class));
73+
}
74+
5975
@Test
6076
public void findMethodAnnotationOnRoot() throws Exception {
61-
Method m = Leaf.class.getMethod("annotatedOnRoot", (Class[]) null);
77+
Method m = Leaf.class.getMethod("annotatedOnRoot");
6278
assertNotNull(m.getAnnotation(Order.class));
6379
assertNotNull(getAnnotation(m, Order.class));
6480
assertNotNull(findAnnotation(m, Order.class));
6581
}
6682

83+
@Test
84+
public void findMethodAnnotationWithMetaAnnotationOnRoot() throws Exception {
85+
Method m = Leaf.class.getMethod("metaAnnotatedOnRoot");
86+
assertNull(m.getAnnotation(Order.class));
87+
assertNotNull(getAnnotation(m, Order.class));
88+
assertNotNull(findAnnotation(m, Order.class));
89+
}
90+
6791
@Test
6892
public void findMethodAnnotationOnRootButOverridden() throws Exception {
69-
Method m = Leaf.class.getMethod("overrideWithoutNewAnnotation", (Class[]) null);
93+
Method m = Leaf.class.getMethod("overrideWithoutNewAnnotation");
7094
assertNull(m.getAnnotation(Order.class));
7195
assertNull(getAnnotation(m, Order.class));
7296
assertNotNull(findAnnotation(m, Order.class));
7397
}
7498

7599
@Test
76100
public void findMethodAnnotationNotAnnotated() throws Exception {
77-
Method m = Leaf.class.getMethod("notAnnotated", (Class[]) null);
101+
Method m = Leaf.class.getMethod("notAnnotated");
78102
assertNull(findAnnotation(m, Order.class));
79103
}
80104

@@ -85,7 +109,7 @@ public void findMethodAnnotationOnBridgeMethod() throws Exception {
85109
assertNull(m.getAnnotation(Order.class));
86110
assertNull(getAnnotation(m, Order.class));
87111
assertNotNull(findAnnotation(m, Order.class));
88-
// TODO: actually found on OpenJDK 8 b99 and higher!
112+
// TODO: getAnnotation() on bridge method actually found on OpenJDK 8 b99 and higher!
89113
// assertNull(m.getAnnotation(Transactional.class));
90114
assertNotNull(getAnnotation(m, Transactional.class));
91115
assertNotNull(findAnnotation(m, Transactional.class));
@@ -462,6 +486,10 @@ public static class Root implements AnnotatedInterface {
462486
public void annotatedOnRoot() {
463487
}
464488

489+
@Meta1
490+
public void metaAnnotatedOnRoot() {
491+
}
492+
465493
public void overrideToAnnotate() {
466494
}
467495

@@ -483,6 +511,14 @@ public static class Leaf extends Root {
483511
public void annotatedOnLeaf() {
484512
}
485513

514+
@Meta1
515+
public void metaAnnotatedOnLeaf() {
516+
}
517+
518+
@MetaMeta
519+
public void metaMetaAnnotatedOnLeaf() {
520+
}
521+
486522
@Override
487523
@Order(1)
488524
public void overrideToAnnotate() {

spring-tx/src/test/java/org/springframework/transaction/event/TransactionalEventListenerTests.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import java.util.Map;
2828

2929
import org.junit.After;
30-
import org.junit.Ignore;
3130
import org.junit.Rule;
3231
import org.junit.Test;
3332
import org.junit.rules.ExpectedException;
@@ -51,7 +50,12 @@
5150
import static org.springframework.transaction.event.TransactionPhase.*;
5251

5352
/**
53+
* Integration tests for {@link TransactionalEventListener @TransactionalEventListener}
54+
* support
55+
*
5456
* @author Stephane Nicoll
57+
* @author Sam Brannen
58+
* @since 4.2
5559
*/
5660
public class TransactionalEventListenerTests {
5761

@@ -265,8 +269,7 @@ public void conditionFoundOnTransactionalEventListener() {
265269
}
266270

267271
@Test
268-
@Ignore("not an event listener if not tagged")
269-
public void afterCommitMetaAnnotation() {
272+
public void afterCommitMetaAnnotation() throws Exception {
270273
load(AfterCommitMetaAnnotationTestListener.class);
271274
this.transactionTemplate.execute(status -> {
272275
getContext().publishEvent("test");
@@ -279,7 +282,6 @@ public void afterCommitMetaAnnotation() {
279282
}
280283

281284
@Test
282-
@Ignore("not an event listener if not tagged + condition found on wrong annotation")
283285
public void conditionFoundOnMetaAnnotation() {
284286
load(AfterCommitMetaAnnotationTestListener.class);
285287
this.transactionTemplate.execute(status -> {

0 commit comments

Comments
 (0)