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 6f3038cbce..fd79c8f736 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 @@ -424,6 +424,7 @@ public class AnnotatedElementUtils { * @param annotationType the annotation type to find * @return the merged, synthesized {@code Annotation}, or {@code null} if not found * @since 4.2 + * @see #findAllMergedAnnotations(AnnotatedElement, Class) * @see #findMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean) * @see #getMergedAnnotationAttributes(AnnotatedElement, Class) */ @@ -460,6 +461,41 @@ public class AnnotatedElementUtils { return AnnotationUtils.synthesizeAnnotation(attributes, (Class) attributes.annotationType(), element); } + /** + * Find 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 result back into an annotation of the specified + * {@code annotationType}. + *

{@link AliasFor @AliasFor} semantics are fully supported, both within a + * single annotation and within the annotation hierarchy. + * @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 #findMergedAnnotation(AnnotatedElement, Class) + */ + public static Set findAllMergedAnnotations(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); + + 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; + } + /** * Find the first annotation of the specified {@code annotationType} within * the annotation hierarchy above the supplied {@code element} and @@ -796,6 +832,8 @@ public class AnnotatedElementUtils { // Locally declared annotations (ignoring @Inherited) Annotation[] annotations = element.getDeclaredAnnotations(); + List aggregatedResults = processor.aggregates() ? new ArrayList() : null; + // Search in local annotations for (Annotation annotation : annotations) { if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation) && @@ -804,7 +842,12 @@ public class AnnotatedElementUtils { metaDepth > 0)) { T result = processor.process(element, annotation, metaDepth); if (result != null) { - return result; + if (processor.aggregates() && metaDepth == 0) { + aggregatedResults.add(result); + } + else { + return result; + } } } } @@ -816,11 +859,20 @@ public class AnnotatedElementUtils { annotation.annotationType(), annotationType, annotationName, processor, visited, metaDepth + 1); if (result != null) { processor.postProcess(annotation.annotationType(), annotation, result); - return result; + if (processor.aggregates() && metaDepth == 0) { + aggregatedResults.add(result); + } + else { + return result; + } } } } + if (processor.aggregates()) { + processor.getAggregatedResults().addAll(0, aggregatedResults); + } + if (element instanceof Method) { Method method = (Method) element; @@ -930,11 +982,16 @@ public class AnnotatedElementUtils { * annotations, or all annotations discovered by the currently executing * search. The term "target" in this context refers to a matching * annotation (i.e., a specific annotation type that was found during - * the search). Returning a non-null value from the {@link #process} + * the search). + *

Returning a non-null value from the {@link #process} * method instructs the search algorithm to stop searching further; * whereas, returning {@code null} from the {@link #process} method * instructs the search algorithm to continue searching for additional - * annotations. + * annotations. One exception to this rule applies to processors + * that {@linkplain #aggregates aggregate} results. If an aggregating + * processor returns a non-null value, that value will be added to the + * list of {@linkplain #getAggregatedResults aggregated results} + * and the search algorithm will continue. *

Processors can optionally {@linkplain #postProcess post-process} * the result of the {@link #process} method as the search algorithm * goes back down the annotation hierarchy from an invocation of @@ -983,12 +1040,38 @@ public class AnnotatedElementUtils { * @param result the result to post-process */ void postProcess(AnnotatedElement annotatedElement, Annotation annotation, T result); + + /** + * 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 + */ + boolean aggregates(); + + /** + * Get the list of results aggregated by this processor. + *

NOTE: the processor does not aggregate the results itself. + * Rather, the search algorithm that uses this processor is 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 + * @since 4.3 + */ + List getAggregatedResults(); } - /** - * {@link Processor} that {@linkplain #process processes} annotations - * but does not {@linkplain #postProcess post-process} results. + * {@link Processor} that {@linkplain #process(AnnotatedElement, Annotation, int) + * processes} annotations but does not {@linkplain #postProcess post-process} or + * {@linkplain #aggregates aggregate} results. * @since 4.2 */ private abstract static class SimpleAnnotationProcessor implements Processor { @@ -997,6 +1080,16 @@ public class AnnotatedElementUtils { public final void postProcess(AnnotatedElement annotatedElement, Annotation annotation, T result) { // no-op } + + @Override + public final boolean aggregates() { + return false; + } + + @Override + public List getAggregatedResults() { + throw new UnsupportedOperationException("SimpleAnnotationProcessor does not support aggregated results"); + } } @@ -1019,13 +1112,33 @@ public class AnnotatedElementUtils { private final boolean nestedAnnotationsAsMap; + private final List aggregatedResults; + + MergedAnnotationAttributesProcessor(Class annotationType, String annotationName, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { + this(annotationType, annotationName, classValuesAsString, nestedAnnotationsAsMap, false); + } + + MergedAnnotationAttributesProcessor(Class annotationType, String annotationName, + boolean classValuesAsString, boolean nestedAnnotationsAsMap, boolean aggregates) { + this.annotationType = annotationType; this.annotationName = annotationName; this.classValuesAsString = classValuesAsString; this.nestedAnnotationsAsMap = nestedAnnotationsAsMap; + this.aggregatedResults = (aggregates ? new ArrayList() : null); + } + + @Override + public boolean aggregates() { + return this.aggregatedResults != null; + } + + @Override + public List getAggregatedResults() { + return this.aggregatedResults; } @Override 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 cec4e506d0..3083793a21 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 @@ -22,6 +22,9 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.Iterator; +import java.util.Set; import org.junit.Test; @@ -43,36 +46,76 @@ public class MultipleComposedAnnotationsOnSingleAnnotatedElementTests { @Test public void multipleComposedAnnotationsOnClass() { - assertMultipleComposedAnnotations(MultipleCachesClass.class); + assertMultipleComposedAnnotations(MultipleComposedCachesClass.class); + } + + @Test + public void composedPlusLocalAnnotationsOnClass() { + assertMultipleComposedAnnotations(ComposedPlusLocalCachesClass.class); + } + + @Test + public void multipleComposedAnnotationsOnInterface() { + assertMultipleComposedAnnotations(MultipleComposedCachesOnInterfaceClass.class); + } + + @Test + public void composedCacheOnInterfaceAndLocalCacheOnClass() { + assertMultipleComposedAnnotations(ComposedCacheOnInterfaceAndLocalCacheClass.class); } @Test public void multipleComposedAnnotationsOnMethod() throws Exception { - AnnotatedElement element = getClass().getDeclaredMethod("multipleCachesMethod"); + AnnotatedElement element = getClass().getDeclaredMethod("multipleComposedCachesMethod"); assertMultipleComposedAnnotations(element); } + @Test + public void composedPlusLocalAnnotationsOnMethod() throws Exception { + AnnotatedElement element = getClass().getDeclaredMethod("composedPlusLocalCachesMethod"); + assertMultipleComposedAnnotations(element); + } + + /** + * Bridge/bridged method setup code copied from + * {@link org.springframework.core.BridgeMethodResolverTests#testWithGenericParameter()}. + */ + @Test + public void multipleComposedAnnotationsBridgeMethod() throws NoSuchMethodException { + Method[] methods = StringGenericParameter.class.getMethods(); + Method bridgeMethod = null; + Method bridgedMethod = null; + + for (Method method : methods) { + if ("getFor".equals(method.getName()) && !method.getParameterTypes()[0].equals(Integer.class)) { + if (method.getReturnType().equals(Object.class)) { + bridgeMethod = method; + } + else { + bridgedMethod = method; + } + } + } + assertTrue(bridgeMethod != null && bridgeMethod.isBridge()); + assertTrue(bridgedMethod != null && !bridgedMethod.isBridge()); + + assertMultipleComposedAnnotations(bridgeMethod); + } + private void assertMultipleComposedAnnotations(AnnotatedElement element) { assertNotNull(element); - // Prerequisites - FooCache fooCache = element.getAnnotation(FooCache.class); - BarCache barCache = element.getAnnotation(BarCache.class); - assertNotNull(fooCache); - assertNotNull(barCache); - assertEquals("fooKey", fooCache.key()); - assertEquals("barKey", barCache.key()); + Set cacheables = findAllMergedAnnotations(element, Cacheable.class); + assertNotNull(cacheables); + assertEquals(2, cacheables.size()); - // Assert the status quo for finding the 1st merged annotation. - Cacheable cacheable = findMergedAnnotation(element, Cacheable.class); - assertNotNull(cacheable); - assertEquals("fooCache", cacheable.value()); - assertEquals("fooKey", cacheable.key()); - - // TODO Introduce findMergedAnnotations(...) in AnnotatedElementUtils. - - // assertEquals("barCache", cacheable.value()); - // assertEquals("barKey", cacheable.key()); + 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()); } @@ -86,7 +129,11 @@ public class MultipleComposedAnnotationsOnSingleAnnotatedElementTests { @Inherited @interface Cacheable { - String value(); + @AliasFor("cacheName") + String value() default ""; + + @AliasFor("value") + String cacheName() default ""; String key() default ""; } @@ -113,13 +160,60 @@ public class MultipleComposedAnnotationsOnSingleAnnotatedElementTests { @FooCache(key = "fooKey") @BarCache(key = "barKey") - private static class MultipleCachesClass { + private static class MultipleComposedCachesClass { + } + + @Cacheable(cacheName = "fooCache", key = "fooKey") + @BarCache(key = "barKey") + private static class ComposedPlusLocalCachesClass { + } + + @FooCache(key = "fooKey") + @BarCache(key = "barKey") + private interface MultipleComposedCachesInterface { + } + + private static class MultipleComposedCachesOnInterfaceClass implements MultipleComposedCachesInterface { + } + + @Cacheable(cacheName = "fooCache", key = "fooKey") + private interface ComposedCacheInterface { + } + + @BarCache(key = "barKey") + private static class ComposedCacheOnInterfaceAndLocalCacheClass implements ComposedCacheInterface { } @FooCache(key = "fooKey") @BarCache(key = "barKey") - private void multipleCachesMethod() { + private void multipleComposedCachesMethod() { + } + + @Cacheable(cacheName = "fooCache", key = "fooKey") + @BarCache(key = "barKey") + private void composedPlusLocalCachesMethod() { + } + + + public interface GenericParameter { + + T getFor(Class cls); + } + + @SuppressWarnings("unused") + private static class StringGenericParameter implements GenericParameter { + + @FooCache(key = "fooKey") + @BarCache(key = "barKey") + @Override + public String getFor(Class cls) { + return "foo"; + } + + public String getFor(Integer integer) { + return "foo"; + } } }