From 1ec35e9c62484df066c2a430684966ee7bf118f0 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Thu, 24 Mar 2016 14:19:36 +0100 Subject: [PATCH] Support searches for multiple merged annotations with "get" semantics This commit picks up where a5139f3c66 left off with added support for "get" search semantics for multiple merged annotations. Specifically, this commit introduces a new getAllMergedAnnotations() method in AnnotatedElementUtils. Issue: SPR-13486 --- .../annotation/AnnotatedElementUtils.java | 91 ++++++--- ...otationsOnSingleAnnotatedElementTests.java | 180 ++++++++++++++++-- 2 files changed, 231 insertions(+), 40 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java index c52937b7a1..2447039dbe 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java @@ -451,6 +451,38 @@ public class AnnotatedElementUtils { return AnnotationUtils.synthesizeAnnotation(attributes, annotationType, element); } + /** + * Get all annotations of the specified {@code annotationType} + * within the annotation hierarchy above the supplied {@code element}; + * and for each annotation found, merge that annotation's attributes with + * matching attributes from annotations in lower levels of the annotation + * hierarchy and synthesize the results back into an annotation of the specified + * {@code annotationType}. + *

{@link AliasFor @AliasFor} semantics are fully supported, both within a + * single annotation and within annotation hierarchies. + *

This method follows get semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element; never {@code null} + * @param annotationType the annotation type to find; never {@code null} + * @return the set of all merged, synthesized {@code Annotations} found, or an empty + * set if none were found + * @since 4.3 + * @see #getMergedAnnotation(AnnotatedElement, Class) + * @see #getAllAnnotationAttributes(AnnotatedElement, String) + * @see #findAllMergedAnnotations(AnnotatedElement, Class) + */ + public static Set getAllMergedAnnotations(AnnotatedElement element, + Class annotationType) { + + Assert.notNull(element, "AnnotatedElement must not be null"); + Assert.notNull(annotationType, "annotationType must not be null"); + + MergedAnnotationAttributesProcessor processor = + new MergedAnnotationAttributesProcessor(annotationType, null, false, false, true); + searchWithGetSemantics(element, annotationType, null, processor); + return postProcessAndSynthesizeAggregatedResults(element, annotationType, processor.getAggregatedResults()); + } + /** * Get the annotation attributes of all annotations of the specified * {@code annotationName} in the annotation hierarchy above the supplied @@ -688,7 +720,7 @@ public class AnnotatedElementUtils { * within the annotation hierarchy above the supplied {@code element}; * and for each annotation found, merge that annotation's attributes with * matching attributes from annotations in lower levels of the annotation - * hierarchy and synthesize the result back into an annotation of the specified + * hierarchy and synthesize the results back into an annotation of the specified * {@code annotationType}. *

{@link AliasFor @AliasFor} semantics are fully supported, both within a * single annotation and within annotation hierarchies. @@ -700,6 +732,7 @@ public class AnnotatedElementUtils { * set if none were found * @since 4.3 * @see #findMergedAnnotation(AnnotatedElement, Class) + * @see #getAllMergedAnnotations(AnnotatedElement, Class) */ public static Set findAllMergedAnnotations(AnnotatedElement element, Class annotationType) { @@ -710,13 +743,7 @@ public class AnnotatedElementUtils { MergedAnnotationAttributesProcessor processor = new MergedAnnotationAttributesProcessor(annotationType, null, false, false, true); searchWithFindSemantics(element, annotationType, annotationType.getName(), processor); - - Set annotations = new LinkedHashSet(); - for (AnnotationAttributes attributes : processor.getAggregatedResults()) { - AnnotationUtils.postProcessAnnotationAttributes(element, attributes, false, false); - annotations.add(AnnotationUtils.synthesizeAnnotation(attributes, annotationType, element)); - } - return annotations; + return postProcessAndSynthesizeAggregatedResults(element, annotationType, processor.getAggregatedResults()); } /** @@ -724,7 +751,7 @@ public class AnnotatedElementUtils { * within the annotation hierarchy above the supplied {@code element}; * and for each annotation found, merge that annotation's attributes with * matching attributes from annotations in lower levels of the annotation - * hierarchy and synthesize the result back into an annotation of the specified + * hierarchy and synthesize the results back into an annotation of the specified * {@code annotationType}. *

The container type that holds the repeatable annotations will be looked up * via {@link java.lang.annotation.Repeatable}. @@ -754,7 +781,7 @@ public class AnnotatedElementUtils { * within the annotation hierarchy above the supplied {@code element}; * and for each annotation found, merge that annotation's attributes with * matching attributes from annotations in lower levels of the annotation - * hierarchy and synthesize the result back into an annotation of the specified + * hierarchy and synthesize the results back into an annotation of the specified * {@code annotationType}. *

{@link AliasFor @AliasFor} semantics are fully supported, both within a * single annotation and within annotation hierarchies. @@ -791,13 +818,7 @@ public class AnnotatedElementUtils { MergedAnnotationAttributesProcessor processor = new MergedAnnotationAttributesProcessor(annotationType, null, false, false, true); searchWithFindSemantics(element, annotationType, annotationType.getName(), containerType, processor); - - Set annotations = new LinkedHashSet(); - for (AnnotationAttributes attributes : processor.getAggregatedResults()) { - AnnotationUtils.postProcessAnnotationAttributes(element, attributes, false, false); - annotations.add(AnnotationUtils.synthesizeAnnotation(attributes, annotationType, element)); - } - return annotations; + return postProcessAndSynthesizeAggregatedResults(element, annotationType, processor.getAggregatedResults()); } /** @@ -906,13 +927,18 @@ public class AnnotatedElementUtils { // Search in annotations for (Annotation annotation : annotations) { + // Note: we only check for (metaDepth > 0) due to the nuances of getMetaAnnotationTypes(). if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation) && - ((annotationType != null ? annotation.annotationType() == annotationType : - annotation.annotationType().getName().equals(annotationName)) || - metaDepth > 0)) { + ((annotation.annotationType() == annotationType + || annotation.annotationType().getName().equals(annotationName)) || metaDepth > 0)) { T result = processor.process(annotatedElement, annotation, metaDepth); if (result != null) { - return result; + if (processor.aggregates() && metaDepth == 0) { + processor.getAggregatedResults().add(result); + } + else { + return result; + } } } } @@ -924,7 +950,12 @@ public class AnnotatedElementUtils { annotationName, processor, visited, metaDepth + 1); if (result != null) { processor.postProcess(annotatedElement, annotation, result); - return result; + if (processor.aggregates() && metaDepth == 0) { + processor.getAggregatedResults().add(result); + } + else { + return result; + } } } } @@ -1234,6 +1265,20 @@ public class AnnotatedElementUtils { } } + /** + * @since 4.3 + */ + private static Set postProcessAndSynthesizeAggregatedResults(AnnotatedElement element, + Class annotationType, List aggregatedResults) { + + Set annotations = new LinkedHashSet(); + for (AnnotationAttributes attributes : aggregatedResults) { + AnnotationUtils.postProcessAnnotationAttributes(element, attributes, false, false); + annotations.add(AnnotationUtils.synthesizeAnnotation(attributes, annotationType, element)); + } + return annotations; + } + /** * Callback interface that is used to process annotations during a search. @@ -1305,7 +1350,6 @@ public class AnnotatedElementUtils { * Determine if this processor aggregates the results returned by {@link #process}. *

If this method returns {@code true}, then {@link #getAggregatedResults()} * must return a non-null value. - *

WARNING: aggregation is currently only supported for find semantics. * @return {@code true} if this processor supports aggregated results * @see #getAggregatedResults * @since 4.3 @@ -1319,7 +1363,6 @@ public class AnnotatedElementUtils { * responsible for asking this processor if it {@link #aggregates} results * and then adding the post-processed results to the list returned by this * method. - *

WARNING: aggregation is currently only supported for find semantics. * @return the list of results aggregated by this processor; never * {@code null} unless {@link #aggregates} returns {@code false} * @see #aggregates diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MultipleComposedAnnotationsOnSingleAnnotatedElementTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MultipleComposedAnnotationsOnSingleAnnotatedElementTests.java index 3083793a21..395b9c31b5 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MultipleComposedAnnotationsOnSingleAnnotatedElementTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MultipleComposedAnnotationsOnSingleAnnotatedElementTests.java @@ -26,6 +26,7 @@ import java.lang.reflect.Method; import java.util.Iterator; import java.util.Set; +import org.junit.Ignore; import org.junit.Test; import static org.junit.Assert.*; @@ -45,43 +46,145 @@ import static org.springframework.core.annotation.AnnotatedElementUtils.*; public class MultipleComposedAnnotationsOnSingleAnnotatedElementTests { @Test - public void multipleComposedAnnotationsOnClass() { - assertMultipleComposedAnnotations(MultipleComposedCachesClass.class); + public void getMultipleComposedAnnotationsOnClass() { + assertGetAllMergedAnnotationsBehavior(MultipleComposedCachesClass.class); } @Test - public void composedPlusLocalAnnotationsOnClass() { - assertMultipleComposedAnnotations(ComposedPlusLocalCachesClass.class); + public void getMultipleInheritedComposedAnnotationsOnSuperclass() { + assertGetAllMergedAnnotationsBehavior(SubMultipleComposedCachesClass.class); } @Test - public void multipleComposedAnnotationsOnInterface() { - assertMultipleComposedAnnotations(MultipleComposedCachesOnInterfaceClass.class); + public void getMultipleNoninheritedComposedAnnotationsOnClass() { + Class element = MultipleNoninheritedComposedCachesClass.class; + Set cacheables = getAllMergedAnnotations(element, Cacheable.class); + assertNotNull(cacheables); + assertEquals(2, cacheables.size()); + + Iterator iterator = cacheables.iterator(); + Cacheable cacheable1 = iterator.next(); + Cacheable cacheable2 = iterator.next(); + assertEquals("noninheritedCache1", cacheable1.value()); + assertEquals("noninheritedCache2", cacheable2.value()); } @Test - public void composedCacheOnInterfaceAndLocalCacheOnClass() { - assertMultipleComposedAnnotations(ComposedCacheOnInterfaceAndLocalCacheClass.class); + public void getMultipleNoninheritedComposedAnnotationsOnSuperclass() { + Class element = SubMultipleNoninheritedComposedCachesClass.class; + Set cacheables = getAllMergedAnnotations(element, Cacheable.class); + assertNotNull(cacheables); + assertEquals(0, cacheables.size()); } @Test - public void multipleComposedAnnotationsOnMethod() throws Exception { + public void getComposedPlusLocalAnnotationsOnClass() { + assertGetAllMergedAnnotationsBehavior(ComposedPlusLocalCachesClass.class); + } + + @Test + public void getMultipleComposedAnnotationsOnInterface() { + Class element = MultipleComposedCachesOnInterfaceClass.class; + Set cacheables = getAllMergedAnnotations(element, Cacheable.class); + assertNotNull(cacheables); + assertEquals(0, cacheables.size()); + } + + @Test + public void getMultipleComposedAnnotationsOnMethod() throws Exception { AnnotatedElement element = getClass().getDeclaredMethod("multipleComposedCachesMethod"); - assertMultipleComposedAnnotations(element); + assertGetAllMergedAnnotationsBehavior(element); } @Test - public void composedPlusLocalAnnotationsOnMethod() throws Exception { + public void getComposedPlusLocalAnnotationsOnMethod() throws Exception { AnnotatedElement element = getClass().getDeclaredMethod("composedPlusLocalCachesMethod"); - assertMultipleComposedAnnotations(element); + assertGetAllMergedAnnotationsBehavior(element); + } + + @Test + @Ignore("Disabled since some Java 8 updates handle the bridge method differently") + public void getMultipleComposedAnnotationsOnBridgeMethod() throws Exception { + Set cacheables = getAllMergedAnnotations(getBridgeMethod(), Cacheable.class); + assertNotNull(cacheables); + assertEquals(0, cacheables.size()); + } + + @Test + public void findMultipleComposedAnnotationsOnClass() { + assertFindAllMergedAnnotationsBehavior(MultipleComposedCachesClass.class); + } + + @Test + public void findMultipleInheritedComposedAnnotationsOnSuperclass() { + assertFindAllMergedAnnotationsBehavior(SubMultipleComposedCachesClass.class); + } + + @Test + public void findMultipleNoninheritedComposedAnnotationsOnClass() { + Class element = MultipleNoninheritedComposedCachesClass.class; + Set cacheables = findAllMergedAnnotations(element, Cacheable.class); + assertNotNull(cacheables); + assertEquals(2, cacheables.size()); + + Iterator iterator = cacheables.iterator(); + Cacheable cacheable1 = iterator.next(); + Cacheable cacheable2 = iterator.next(); + assertEquals("noninheritedCache1", cacheable1.value()); + assertEquals("noninheritedCache2", cacheable2.value()); + } + + @Test + public void findMultipleNoninheritedComposedAnnotationsOnSuperclass() { + Class element = SubMultipleNoninheritedComposedCachesClass.class; + Set cacheables = findAllMergedAnnotations(element, Cacheable.class); + assertNotNull(cacheables); + assertEquals(2, cacheables.size()); + + Iterator iterator = cacheables.iterator(); + Cacheable cacheable1 = iterator.next(); + Cacheable cacheable2 = iterator.next(); + assertEquals("noninheritedCache1", cacheable1.value()); + assertEquals("noninheritedCache2", cacheable2.value()); + } + + @Test + public void findComposedPlusLocalAnnotationsOnClass() { + assertFindAllMergedAnnotationsBehavior(ComposedPlusLocalCachesClass.class); + } + + @Test + public void findMultipleComposedAnnotationsOnInterface() { + assertFindAllMergedAnnotationsBehavior(MultipleComposedCachesOnInterfaceClass.class); + } + + @Test + public void findComposedCacheOnInterfaceAndLocalCacheOnClass() { + assertFindAllMergedAnnotationsBehavior(ComposedCacheOnInterfaceAndLocalCacheClass.class); + } + + @Test + public void findMultipleComposedAnnotationsOnMethod() throws Exception { + AnnotatedElement element = getClass().getDeclaredMethod("multipleComposedCachesMethod"); + assertFindAllMergedAnnotationsBehavior(element); + } + + @Test + public void findComposedPlusLocalAnnotationsOnMethod() throws Exception { + AnnotatedElement element = getClass().getDeclaredMethod("composedPlusLocalCachesMethod"); + assertFindAllMergedAnnotationsBehavior(element); + } + + @Test + public void findMultipleComposedAnnotationsOnBridgeMethod() throws Exception { + assertFindAllMergedAnnotationsBehavior(getBridgeMethod()); } /** * Bridge/bridged method setup code copied from * {@link org.springframework.core.BridgeMethodResolverTests#testWithGenericParameter()}. */ - @Test - public void multipleComposedAnnotationsBridgeMethod() throws NoSuchMethodException { + public Method getBridgeMethod() throws NoSuchMethodException { Method[] methods = StringGenericParameter.class.getMethods(); Method bridgeMethod = null; Method bridgedMethod = null; @@ -99,10 +202,26 @@ public class MultipleComposedAnnotationsOnSingleAnnotatedElementTests { assertTrue(bridgeMethod != null && bridgeMethod.isBridge()); assertTrue(bridgedMethod != null && !bridgedMethod.isBridge()); - assertMultipleComposedAnnotations(bridgeMethod); + return bridgeMethod; } - private void assertMultipleComposedAnnotations(AnnotatedElement element) { + private void assertGetAllMergedAnnotationsBehavior(AnnotatedElement element) { + assertNotNull(element); + + Set cacheables = getAllMergedAnnotations(element, Cacheable.class); + assertNotNull(cacheables); + assertEquals(2, cacheables.size()); + + Iterator iterator = cacheables.iterator(); + Cacheable fooCacheable = iterator.next(); + Cacheable barCacheable = iterator.next(); + assertEquals("fooKey", fooCacheable.key()); + assertEquals("fooCache", fooCacheable.value()); + assertEquals("barKey", barCacheable.key()); + assertEquals("barCache", barCacheable.value()); + } + + private void assertFindAllMergedAnnotationsBehavior(AnnotatedElement element) { assertNotNull(element); Set cacheables = findAllMergedAnnotations(element, Cacheable.class); @@ -158,11 +277,40 @@ public class MultipleComposedAnnotationsOnSingleAnnotatedElementTests { String key(); } + @Cacheable("noninheritedCache1") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @interface NoninheritedCache1 { + + @AliasFor(annotation = Cacheable.class) + String key() default ""; + } + + @Cacheable("noninheritedCache2") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @interface NoninheritedCache2 { + + @AliasFor(annotation = Cacheable.class) + String key() default ""; + } + @FooCache(key = "fooKey") @BarCache(key = "barKey") private static class MultipleComposedCachesClass { } + private static class SubMultipleComposedCachesClass extends MultipleComposedCachesClass { + } + + @NoninheritedCache1 + @NoninheritedCache2 + private static class MultipleNoninheritedComposedCachesClass { + } + + private static class SubMultipleNoninheritedComposedCachesClass extends MultipleNoninheritedComposedCachesClass { + } + @Cacheable(cacheName = "fooCache", key = "fooKey") @BarCache(key = "barKey") private static class ComposedPlusLocalCachesClass {