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 0caaa805e70..dd10bd53489 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 @@ -33,8 +33,8 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; /** - * General utility methods for finding annotations and meta-annotations on - * {@link AnnotatedElement AnnotatedElements}. + * General utility methods for finding annotations, meta-annotations, and + * repeatable annotations on {@link AnnotatedElement AnnotatedElements}. * *
{@code AnnotatedElementUtils} defines the public API for Spring's
* meta-annotation programming model with support for annotation attribute
@@ -48,7 +48,8 @@ import org.springframework.util.MultiValueMap;
* Support for meta-annotations with attribute overrides in
* composed annotations is provided by all variants of the
* {@code getMergedAnnotationAttributes()}, {@code getMergedAnnotation()},
- * {@code findMergedAnnotationAttributes()}, and {@code findMergedAnnotation()}
+ * {@code findMergedAnnotationAttributes()}, {@code findMergedAnnotation()},
+ * {@code findAllMergedAnnotations()}, and {@code findMergedRepeatableAnnotations()}
* methods.
*
* {@link AliasFor @AliasFor} semantics are fully supported, both
* within a single annotation and within the annotation hierarchy.
+ * This method follows find semantics as described in the
+ * {@linkplain AnnotatedElementUtils class-level javadoc}.
* @param element the annotated element
* @param annotationType the annotation type to find
* @return the merged, synthesized {@code Annotation}, or {@code null} if not found
@@ -507,6 +513,8 @@ public class AnnotatedElementUtils {
* This method delegates to {@link #findMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean)}
* (supplying {@code false} for {@code classValuesAsString} and {@code nestedAnnotationsAsMap})
* and {@link AnnotationUtils#synthesizeAnnotation(Map, Class, AnnotatedElement)}.
+ * This method follows find semantics as described in the
+ * {@linkplain AnnotatedElementUtils class-level javadoc}.
* @param element the annotated element
* @param annotationName the fully qualified class name of the annotation type to find
* @return the merged, synthesized {@code Annotation}, or {@code null} if not found
@@ -528,10 +536,12 @@ 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 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.
+ * single annotation and within annotation hierarchies.
+ * This method follows find 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
@@ -557,6 +567,93 @@ public class AnnotatedElementUtils {
return annotations;
}
+ /**
+ * Find all repeatable 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}.
+ * The container type that holds the repeatable annotations will be looked up
+ * via {@link java.lang.annotation.Repeatable}.
+ * {@link AliasFor @AliasFor} semantics are fully supported, both within a
+ * single annotation and within annotation hierarchies.
+ * This method follows find 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 repeatable {@code Annotations} found, or an empty
+ * set if none were found
+ * @since 4.3
+ * @see #findMergedAnnotation(AnnotatedElement, Class)
+ * @see #findAllMergedAnnotations(AnnotatedElement, Class)
+ * @see #findMergedRepeatableAnnotations(AnnotatedElement, Class, Class)
+ * @throws IllegalArgumentException if the {@code element} or {@code annotationType}
+ * is {@code null}, or if the container type cannot be resolved
+ */
+ public static Set findMergedRepeatableAnnotations(AnnotatedElement element,
+ Class annotationType) {
+
+ return findMergedRepeatableAnnotations(element, annotationType, null);
+ }
+
+ /**
+ * Find all repeatable 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 annotation hierarchies.
+ * This method follows find 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}
+ * @param containerType the type of the container that holds the annotations;
+ * may be {@code null} if the container type should be looked up via
+ * {@link java.lang.annotation.Repeatable}
+ * @return the set of all merged repeatable {@code Annotations} found, or an empty
+ * set if none were found
+ * @since 4.3
+ * @see #findMergedAnnotation(AnnotatedElement, Class)
+ * @see #findAllMergedAnnotations(AnnotatedElement, Class)
+ * @throws IllegalArgumentException if the {@code element} or {@code annotationType}
+ * is {@code null}, or if the container type cannot be resolved
+ * @throws AnnotationConfigurationException if the supplied {@code containerType}
+ * is not a valid container annotation for the supplied {@code annotationType}
+ */
+ public static Set findMergedRepeatableAnnotations(AnnotatedElement element,
+ Class annotationType, Class extends Annotation> containerType) {
+
+ Assert.notNull(element, "AnnotatedElement must not be null");
+ Assert.notNull(annotationType, "annotationType must not be null");
+
+ if (containerType == null) {
+ containerType = AnnotationUtils.resolveContainerAnnotationType(annotationType);
+ if (containerType == null) {
+ throw new IllegalArgumentException(
+ "annotationType must be a repeatable annotation: failed to resolve container type for "
+ + annotationType.getName());
+ }
+ }
+ else {
+ validateRepeatableContainerType(annotationType, containerType);
+ }
+
+ 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;
+ }
+
/**
* Find the first annotation of the specified {@code annotationType} within
* the annotation hierarchy above the supplied {@code element} and
@@ -853,12 +950,37 @@ public class AnnotatedElementUtils {
* @return the result of the processor, potentially {@code null}
* @since 4.2
*/
- private static 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
+ * 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
@@ -1158,6 +1346,8 @@ public class AnnotatedElementUtils {
* target annotation during the {@link #process} phase and then merges
* annotation attributes from lower levels in the annotation hierarchy
* during the {@link #postProcess} phase.
+ * A {@code MergedAnnotationAttributesProcessor} may optionally be
+ * configured to {@linkplain #aggregates aggregate} results.
* @since 4.2
* @see AnnotationUtils#retrieveAnnotationAttributes
* @see AnnotationUtils#postProcessAnnotationAttributes
@@ -1172,6 +1362,8 @@ public class AnnotatedElementUtils {
private final boolean nestedAnnotationsAsMap;
+ private final boolean aggregates;
+
private final List Automatically detects a container annotation declared via
+ * {@link java.lang.annotation.Repeatable}. If the supplied annotation type
+ * is not annotated with {@code @Repeatable}, this method simply returns
+ * {@code null}.
+ * @since 4.2
+ */
+ @SuppressWarnings("unchecked")
+ static Class extends Annotation> resolveContainerAnnotationType(Class extends Annotation> annotationType) {
+ try {
+ Annotation repeatable = getAnnotation(annotationType, REPEATABLE_CLASS_NAME);
+ if (repeatable != null) {
+ Object value = AnnotationUtils.getValue(repeatable);
+ return (Class extends Annotation>) value;
+ }
+ }
+ catch (Exception ex) {
+ handleIntrospectionFailure(annotationType, ex);
+ }
+ return null;
+ }
+
/**
* If the supplied throwable is an {@link AnnotationConfigurationException},
* it will be cast to an {@code AnnotationConfigurationException} and thrown,
@@ -1806,8 +1830,6 @@ public abstract class AnnotationUtils {
private static class AnnotationCollector {
- private static final String REPEATABLE_CLASS_NAME = "java.lang.annotation.Repeatable";
-
private final Class annotationType;
private final Class extends Annotation> containerAnnotationType;
@@ -1825,21 +1847,6 @@ public abstract class AnnotationUtils {
this.declaredMode = declaredMode;
}
- @SuppressWarnings("unchecked")
- static Class extends Annotation> resolveContainerAnnotationType(Class extends Annotation> annotationType) {
- try {
- Annotation repeatable = getAnnotation(annotationType, REPEATABLE_CLASS_NAME);
- if (repeatable != null) {
- Object value = AnnotationUtils.getValue(repeatable);
- return (Class extends Annotation>) value;
- }
- }
- catch (Exception ex) {
- handleIntrospectionFailure(annotationType, ex);
- }
- return null;
- }
-
Set getResult(AnnotatedElement element) {
process(element);
return Collections.unmodifiableSet(this.result);
diff --git a/spring-core/src/test/java/org/springframework/core/annotation/ComposedRepeatableAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/ComposedRepeatableAnnotationsTests.java
new file mode 100644
index 00000000000..e532e5e571a
--- /dev/null
+++ b/spring-core/src/test/java/org/springframework/core/annotation/ComposedRepeatableAnnotationsTests.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2002-2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.core.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.AnnotatedElement;
+import java.util.Iterator;
+import java.util.Set;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.hamcrest.CoreMatchers.isA;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.Assert.*;
+import static org.springframework.core.annotation.AnnotatedElementUtils.*;
+
+/**
+ * Unit tests that verify support for finding all composed, repeatable
+ * annotations on a single annotated element.
+ *
+ * See SPR-13973.
+ *
+ * @author Sam Brannen
+ * @since 4.3
+ * @see AnnotatedElementUtils
+ * @see AnnotatedElementUtilsTests
+ */
+public class ComposedRepeatableAnnotationsTests {
+
+ @Rule
+ public final ExpectedException exception = ExpectedException.none();
+
+
+ @Test
+ public void nonRepeatableAnnotation() {
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage(startsWith("annotationType must be a repeatable annotation"));
+ exception.expectMessage(containsString("failed to resolve container type for"));
+ exception.expectMessage(containsString(NonRepeatable.class.getName()));
+ findMergedRepeatableAnnotations(getClass(), NonRepeatable.class);
+ }
+
+ @Test
+ public void invalidRepeatableAnnotationContainerMissingValueAttribute() {
+ exception.expect(AnnotationConfigurationException.class);
+ exception.expectMessage(startsWith("Invalid declaration of container type"));
+ exception.expectMessage(containsString(ContainerMissingValueAttribute.class.getName()));
+ exception.expectMessage(containsString("for repeatable annotation"));
+ exception.expectMessage(containsString(InvalidRepeatable.class.getName()));
+ exception.expectCause(isA(NoSuchMethodException.class));
+ findMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, ContainerMissingValueAttribute.class);
+ }
+
+ @Test
+ public void invalidRepeatableAnnotationContainerWithNonArrayValueAttribute() {
+ exception.expect(AnnotationConfigurationException.class);
+ exception.expectMessage(startsWith("Container type"));
+ exception.expectMessage(containsString(ContainerWithNonArrayValueAttribute.class.getName()));
+ exception.expectMessage(containsString("must declare a 'value' attribute for an array of type"));
+ exception.expectMessage(containsString(InvalidRepeatable.class.getName()));
+ findMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, ContainerWithNonArrayValueAttribute.class);
+ }
+
+ @Test
+ public void invalidRepeatableAnnotationContainerWithArrayValueAttributeButWrongComponentType() {
+ exception.expect(AnnotationConfigurationException.class);
+ exception.expectMessage(startsWith("Container type"));
+ exception.expectMessage(containsString(ContainerWithArrayValueAttributeButWrongComponentType.class.getName()));
+ exception.expectMessage(containsString("must declare a 'value' attribute for an array of type"));
+ exception.expectMessage(containsString(InvalidRepeatable.class.getName()));
+ findMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class,
+ ContainerWithArrayValueAttributeButWrongComponentType.class);
+ }
+
+ @Test
+ public void repeatableAnnotationsOnClass() {
+ assertRepeatableAnnotations(RepeatableClass.class);
+ }
+
+ @Test
+ public void repeatableAnnotationsOnSuperclass() {
+ assertRepeatableAnnotations(SubRepeatableClass.class);
+ }
+
+ @Test
+ public void composedRepeatableAnnotationsOnClass() {
+ assertRepeatableAnnotations(ComposedRepeatableClass.class);
+ }
+
+ @Test
+ public void composedRepeatableAnnotationsMixedWithContainerOnClass() {
+ assertRepeatableAnnotations(ComposedRepeatableMixedWithContainerClass.class);
+ }
+
+ @Test
+ public void composedContainerForRepeatableAnnotationsOnClass() {
+ assertRepeatableAnnotations(ComposedContainerClass.class);
+ }
+
+ private void assertRepeatableAnnotations(AnnotatedElement element) {
+ assertNotNull(element);
+
+ SetFind vs. Get Semantics
@@ -96,6 +97,7 @@ public class AnnotatedElementUtils {
private static final Boolean CONTINUE = null;
+ private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0];
/**
* Build an adapted {@link AnnotatedElement} for the given annotations,
@@ -266,6 +268,7 @@ public class AnnotatedElementUtils {
* @param annotationType the annotation type to find
* @return {@code true} if a matching annotation is present
* @since 4.2.3
+ * @see #hasAnnotation(AnnotatedElement, Class)
*/
public static boolean isAnnotated(AnnotatedElement element, final Class extends Annotation> annotationType) {
Assert.notNull(element, "AnnotatedElement must not be null");
@@ -442,6 +445,7 @@ public class AnnotatedElementUtils {
* @param annotationType the annotation type to find
* @return {@code true} if a matching annotation is present
* @since 4.3
+ * @see #isAnnotated(AnnotatedElement, Class)
*/
public static boolean hasAnnotation(AnnotatedElement element, final Class extends Annotation> annotationType) {
Assert.notNull(element, "AnnotatedElement must not be null");
@@ -470,6 +474,8 @@ public class AnnotatedElementUtils {
* the result back into an annotation of the specified {@code annotationType}.
*