Support repeatable annotations as composed annotations

Prior to this commit, AnnotationUtils supported searching for
repeatable annotations even if the repeatable annotation was declared
on a custom stereotype annotation. However, there was no support for
merging of attributes in composed repeatable annotations. In other
words, it was not possible for a custom annotation to override
attributes in a repeatable annotation.

This commit addresses this by introducing
findMergedRepeatableAnnotations() methods in AnnotatedElementUtils.
These new methods provide full support for explicit annotation
attribute overrides configured via @AliasFor (as well as
convention-based overrides) with "find semantics".

Issue: SPR-13973
This commit is contained in:
Sam Brannen 2016-03-23 20:34:43 +01:00
parent 63115ed6eb
commit 2535469099
3 changed files with 487 additions and 57 deletions

View File

@ -33,8 +33,8 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
/** /**
* General utility methods for finding annotations and meta-annotations on * General utility methods for finding annotations, meta-annotations, and
* {@link AnnotatedElement AnnotatedElements}. * repeatable annotations on {@link AnnotatedElement AnnotatedElements}.
* *
* <p>{@code AnnotatedElementUtils} defines the public API for Spring's * <p>{@code AnnotatedElementUtils} defines the public API for Spring's
* meta-annotation programming model with support for <em>annotation attribute * meta-annotation programming model with support for <em>annotation attribute
@ -48,7 +48,8 @@ import org.springframework.util.MultiValueMap;
* <p>Support for meta-annotations with <em>attribute overrides</em> in * <p>Support for meta-annotations with <em>attribute overrides</em> in
* <em>composed annotations</em> is provided by all variants of the * <em>composed annotations</em> is provided by all variants of the
* {@code getMergedAnnotationAttributes()}, {@code getMergedAnnotation()}, * {@code getMergedAnnotationAttributes()}, {@code getMergedAnnotation()},
* {@code findMergedAnnotationAttributes()}, and {@code findMergedAnnotation()} * {@code findMergedAnnotationAttributes()}, {@code findMergedAnnotation()},
* {@code findAllMergedAnnotations()}, and {@code findMergedRepeatableAnnotations()}
* methods. * methods.
* *
* <h3>Find vs. Get Semantics</h3> * <h3>Find vs. Get Semantics</h3>
@ -96,6 +97,7 @@ public class AnnotatedElementUtils {
private static final Boolean CONTINUE = null; 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, * Build an adapted {@link AnnotatedElement} for the given annotations,
@ -266,6 +268,7 @@ public class AnnotatedElementUtils {
* @param annotationType the annotation type to find * @param annotationType the annotation type to find
* @return {@code true} if a matching annotation is present * @return {@code true} if a matching annotation is present
* @since 4.2.3 * @since 4.2.3
* @see #hasAnnotation(AnnotatedElement, Class)
*/ */
public static boolean isAnnotated(AnnotatedElement element, final Class<? extends Annotation> annotationType) { public static boolean isAnnotated(AnnotatedElement element, final Class<? extends Annotation> annotationType) {
Assert.notNull(element, "AnnotatedElement must not be null"); Assert.notNull(element, "AnnotatedElement must not be null");
@ -442,6 +445,7 @@ public class AnnotatedElementUtils {
* @param annotationType the annotation type to find * @param annotationType the annotation type to find
* @return {@code true} if a matching annotation is present * @return {@code true} if a matching annotation is present
* @since 4.3 * @since 4.3
* @see #isAnnotated(AnnotatedElement, Class)
*/ */
public static boolean hasAnnotation(AnnotatedElement element, final Class<? extends Annotation> annotationType) { public static boolean hasAnnotation(AnnotatedElement element, final Class<? extends Annotation> annotationType) {
Assert.notNull(element, "AnnotatedElement must not be null"); 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}. * the result back into an annotation of the specified {@code annotationType}.
* <p>{@link AliasFor @AliasFor} semantics are fully supported, both * <p>{@link AliasFor @AliasFor} semantics are fully supported, both
* within a single annotation and within the annotation hierarchy. * within a single annotation and within the annotation hierarchy.
* <p>This method follows <em>find semantics</em> as described in the
* {@linkplain AnnotatedElementUtils class-level javadoc}.
* @param element the annotated element * @param element the annotated element
* @param annotationType the annotation type to find * @param annotationType the annotation type to find
* @return the merged, synthesized {@code Annotation}, or {@code null} if not found * @return the merged, synthesized {@code Annotation}, or {@code null} if not found
@ -507,6 +513,8 @@ public class AnnotatedElementUtils {
* <p>This method delegates to {@link #findMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean)} * <p>This method delegates to {@link #findMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean)}
* (supplying {@code false} for {@code classValuesAsString} and {@code nestedAnnotationsAsMap}) * (supplying {@code false} for {@code classValuesAsString} and {@code nestedAnnotationsAsMap})
* and {@link AnnotationUtils#synthesizeAnnotation(Map, Class, AnnotatedElement)}. * and {@link AnnotationUtils#synthesizeAnnotation(Map, Class, AnnotatedElement)}.
* <p>This method follows <em>find semantics</em> as described in the
* {@linkplain AnnotatedElementUtils class-level javadoc}.
* @param element the annotated element * @param element the annotated element
* @param annotationName the fully qualified class name of the annotation type to find * @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 * @return the merged, synthesized {@code Annotation}, or {@code null} if not found
@ -528,10 +536,12 @@ public class AnnotatedElementUtils {
* within the annotation hierarchy <em>above</em> the supplied {@code element}; * within the annotation hierarchy <em>above</em> the supplied {@code element};
* and for each annotation found, merge that annotation's attributes with * and for each annotation found, merge that annotation's attributes with
* <em>matching</em> attributes from annotations in lower levels of the annotation * <em>matching</em> 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}. * {@code annotationType}.
* <p>{@link AliasFor @AliasFor} semantics are fully supported, both within a * <p>{@link AliasFor @AliasFor} semantics are fully supported, both within a
* single annotation and within the annotation hierarchy. * single annotation and within annotation hierarchies.
* <p>This method follows <em>find semantics</em> as described in the
* {@linkplain AnnotatedElementUtils class-level javadoc}.
* @param element the annotated element; never {@code null} * @param element the annotated element; never {@code null}
* @param annotationType the annotation type to find; 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 * @return the set of all merged, synthesized {@code Annotations} found, or an empty
@ -557,6 +567,93 @@ public class AnnotatedElementUtils {
return annotations; return annotations;
} }
/**
* Find all <em>repeatable annotations</em> of the specified {@code annotationType}
* within the annotation hierarchy <em>above</em> the supplied {@code element};
* and for each annotation found, merge that annotation's attributes with
* <em>matching</em> attributes from annotations in lower levels of the annotation
* hierarchy and synthesize the result back into an annotation of the specified
* {@code annotationType}.
* <p>The container type that holds the repeatable annotations will be looked up
* via {@link java.lang.annotation.Repeatable}.
* <p>{@link AliasFor @AliasFor} semantics are fully supported, both within a
* single annotation and within annotation hierarchies.
* <p>This method follows <em>find semantics</em> 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 <A extends Annotation> Set<A> findMergedRepeatableAnnotations(AnnotatedElement element,
Class<A> annotationType) {
return findMergedRepeatableAnnotations(element, annotationType, null);
}
/**
* Find all <em>repeatable annotations</em> of the specified {@code annotationType}
* within the annotation hierarchy <em>above</em> the supplied {@code element};
* and for each annotation found, merge that annotation's attributes with
* <em>matching</em> attributes from annotations in lower levels of the annotation
* hierarchy and synthesize the result back into an annotation of the specified
* {@code annotationType}.
* <p>{@link AliasFor @AliasFor} semantics are fully supported, both within a
* single annotation and within annotation hierarchies.
* <p>This method follows <em>find semantics</em> 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 <A extends Annotation> Set<A> findMergedRepeatableAnnotations(AnnotatedElement element,
Class<A> 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<A> annotations = new LinkedHashSet<A>();
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 * Find the first annotation of the specified {@code annotationType} within
* the annotation hierarchy <em>above</em> the supplied {@code element} and * the annotation hierarchy <em>above</em> the supplied {@code element} and
@ -853,12 +950,37 @@ public class AnnotatedElementUtils {
* @return the result of the processor, potentially {@code null} * @return the result of the processor, potentially {@code null}
* @since 4.2 * @since 4.2
*/ */
private static <T> T searchWithFindSemantics( private static <T> T searchWithFindSemantics(AnnotatedElement element, Class<? extends Annotation> annotationType,
AnnotatedElement element, Class<? extends Annotation> annotationType, String annotationName, Processor<T> processor) { String annotationName, Processor<T> processor) {
return searchWithFindSemantics(element, annotationType, annotationName, null, processor);
}
/**
* Search for annotations of the specified {@code annotationName} or
* {@code annotationType} on the specified {@code element}, following
* <em>find semantics</em>.
* @param element the annotated element
* @param annotationType the annotation type to find
* @param annotationName the fully qualified class name of the annotation
* type to find (as an alternative to {@code annotationType})
* @param containerType the type of the container that holds repeatable
* annotations, or {@code null} if the annotation is not repeatable
* @param processor the processor to delegate to
* @return the result of the processor, potentially {@code null}
* @since 4.2
*/
private static <T> T searchWithFindSemantics(AnnotatedElement element, Class<? extends Annotation> annotationType,
String annotationName, Class<? extends Annotation> containerType, Processor<T> processor) {
if (containerType != null && !processor.aggregates()) {
throw new IllegalArgumentException(
"Searches for repeatable annotations must supply an aggregating Processor");
}
try { try {
return searchWithFindSemantics( return searchWithFindSemantics(
element, annotationType, annotationName, processor, new HashSet<AnnotatedElement>(), 0); element, annotationType, annotationName, containerType, processor, new HashSet<AnnotatedElement>(), 0);
} }
catch (Throwable ex) { catch (Throwable ex) {
AnnotationUtils.rethrowAnnotationConfigurationException(ex); AnnotationUtils.rethrowAnnotationConfigurationException(ex);
@ -876,6 +998,8 @@ public class AnnotatedElementUtils {
* @param annotationType the annotation type to find * @param annotationType the annotation type to find
* @param annotationName the fully qualified class name of the annotation * @param annotationName the fully qualified class name of the annotation
* type to find (as an alternative to {@code annotationType}) * type to find (as an alternative to {@code annotationType})
* @param containerType the type of the container that holds repeatable
* annotations, or {@code null} if the annotation is not repeatable
* @param processor the processor to delegate to * @param processor the processor to delegate to
* @param visited the set of annotated elements that have already been visited * @param visited the set of annotated elements that have already been visited
* @param metaDepth the meta-depth of the annotation * @param metaDepth the meta-depth of the annotation
@ -883,7 +1007,8 @@ public class AnnotatedElementUtils {
* @since 4.2 * @since 4.2
*/ */
private static <T> T searchWithFindSemantics(AnnotatedElement element, Class<? extends Annotation> annotationType, private static <T> T searchWithFindSemantics(AnnotatedElement element, Class<? extends Annotation> annotationType,
String annotationName, Processor<T> processor, Set<AnnotatedElement> visited, int metaDepth) { String annotationName, Class<? extends Annotation> containerType, Processor<T> processor,
Set<AnnotatedElement> visited, int metaDepth) {
Assert.notNull(element, "AnnotatedElement must not be null"); Assert.notNull(element, "AnnotatedElement must not be null");
Assert.hasLength(annotationName, "annotationName must not be null or empty"); Assert.hasLength(annotationName, "annotationName must not be null or empty");
@ -896,10 +1021,10 @@ public class AnnotatedElementUtils {
// Search in local annotations // Search in local annotations
for (Annotation annotation : annotations) { for (Annotation annotation : annotations) {
if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation) && if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) {
((annotationType != null ? annotation.annotationType() == annotationType : if (annotation.annotationType() == annotationType
annotation.annotationType().getName().equals(annotationName)) || || annotation.annotationType().getName().equals(annotationName)) {
metaDepth > 0)) {
T result = processor.process(element, annotation, metaDepth); T result = processor.process(element, annotation, metaDepth);
if (result != null) { if (result != null) {
if (processor.aggregates() && metaDepth == 0) { if (processor.aggregates() && metaDepth == 0) {
@ -910,13 +1035,25 @@ public class AnnotatedElementUtils {
} }
} }
} }
// Repeatable annotations in container?
else if (annotation.annotationType() == containerType) {
for (Annotation contained : getRawAnnotationsFromContainer(element, annotation)) {
T result = processor.process(element, contained, metaDepth);
if (result != null) {
// No need to post-process since repeatable annotations within a
// container cannot be composed annotations.
aggregatedResults.add(result);
}
}
}
}
} }
// Search in meta annotations on local annotations // Search in meta annotations on local annotations
for (Annotation annotation : annotations) { for (Annotation annotation : annotations) {
if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) { if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) {
T result = searchWithFindSemantics( T result = searchWithFindSemantics(annotation.annotationType(), annotationType, annotationName,
annotation.annotationType(), annotationType, annotationName, processor, visited, metaDepth + 1); containerType, processor, visited, metaDepth + 1);
if (result != null) { if (result != null) {
processor.postProcess(annotation.annotationType(), annotation, result); processor.postProcess(annotation.annotationType(), annotation, result);
if (processor.aggregates() && metaDepth == 0) { if (processor.aggregates() && metaDepth == 0) {
@ -930,6 +1067,7 @@ public class AnnotatedElementUtils {
} }
if (processor.aggregates()) { if (processor.aggregates()) {
// Prepend to support top-down ordering within class hierarchies
processor.getAggregatedResults().addAll(0, aggregatedResults); processor.getAggregatedResults().addAll(0, aggregatedResults);
} }
@ -938,7 +1076,7 @@ public class AnnotatedElementUtils {
// Search on possibly bridged method // Search on possibly bridged method
Method resolvedMethod = BridgeMethodResolver.findBridgedMethod(method); Method resolvedMethod = BridgeMethodResolver.findBridgedMethod(method);
T result = searchWithFindSemantics(resolvedMethod, annotationType, annotationName, T result = searchWithFindSemantics(resolvedMethod, annotationType, annotationName, containerType,
processor, visited, metaDepth); processor, visited, metaDepth);
if (result != null) { if (result != null) {
return result; return result;
@ -946,8 +1084,8 @@ public class AnnotatedElementUtils {
// Search on methods in interfaces declared locally // Search on methods in interfaces declared locally
Class<?>[] ifcs = method.getDeclaringClass().getInterfaces(); Class<?>[] ifcs = method.getDeclaringClass().getInterfaces();
result = searchOnInterfaces( result = searchOnInterfaces(method, annotationType, annotationName, containerType, processor,
method, annotationType, annotationName, processor, visited, metaDepth, ifcs); visited, metaDepth, ifcs);
if (result != null) { if (result != null) {
return result; return result;
} }
@ -964,7 +1102,7 @@ public class AnnotatedElementUtils {
Method equivalentMethod = clazz.getDeclaredMethod(method.getName(), method.getParameterTypes()); Method equivalentMethod = clazz.getDeclaredMethod(method.getName(), method.getParameterTypes());
Method resolvedEquivalentMethod = BridgeMethodResolver.findBridgedMethod(equivalentMethod); Method resolvedEquivalentMethod = BridgeMethodResolver.findBridgedMethod(equivalentMethod);
result = searchWithFindSemantics(resolvedEquivalentMethod, annotationType, annotationName, result = searchWithFindSemantics(resolvedEquivalentMethod, annotationType, annotationName,
processor, visited, metaDepth); containerType, processor, visited, metaDepth);
if (result != null) { if (result != null) {
return result; return result;
} }
@ -974,8 +1112,8 @@ public class AnnotatedElementUtils {
} }
// Search on interfaces declared on superclass // Search on interfaces declared on superclass
result = searchOnInterfaces(method, annotationType, annotationName, processor, visited, result = searchOnInterfaces(method, annotationType, annotationName, containerType, processor,
metaDepth, clazz.getInterfaces()); visited, metaDepth, clazz.getInterfaces());
if (result != null) { if (result != null) {
return result; return result;
} }
@ -987,8 +1125,8 @@ public class AnnotatedElementUtils {
// Search on interfaces // Search on interfaces
for (Class<?> ifc : clazz.getInterfaces()) { for (Class<?> ifc : clazz.getInterfaces()) {
T result = searchWithFindSemantics( T result = searchWithFindSemantics(ifc, annotationType, annotationName, containerType,
ifc, annotationType, annotationName, processor, visited, metaDepth); processor, visited, metaDepth);
if (result != null) { if (result != null) {
return result; return result;
} }
@ -997,8 +1135,8 @@ public class AnnotatedElementUtils {
// Search on superclass // Search on superclass
Class<?> superclass = clazz.getSuperclass(); Class<?> superclass = clazz.getSuperclass();
if (superclass != null && Object.class != superclass) { if (superclass != null && Object.class != superclass) {
T result = searchWithFindSemantics( T result = searchWithFindSemantics(superclass, annotationType, annotationName, containerType,
superclass, annotationType, annotationName, processor, visited, metaDepth); processor, visited, metaDepth);
if (result != null) { if (result != null) {
return result; return result;
} }
@ -1012,14 +1150,15 @@ public class AnnotatedElementUtils {
return null; return null;
} }
private static <T> T searchOnInterfaces(Method method, Class<? extends Annotation> annotationType, String annotationName, private static <T> T searchOnInterfaces(Method method, Class<? extends Annotation> annotationType,
Processor<T> processor, Set<AnnotatedElement> visited, int metaDepth, Class<?>[] ifcs) { String annotationName, Class<? extends Annotation> containerType, Processor<T> processor,
Set<AnnotatedElement> visited, int metaDepth, Class<?>[] ifcs) {
for (Class<?> iface : ifcs) { for (Class<?> iface : ifcs) {
if (AnnotationUtils.isInterfaceWithAnnotatedMethods(iface)) { if (AnnotationUtils.isInterfaceWithAnnotatedMethods(iface)) {
try { try {
Method equivalentMethod = iface.getMethod(method.getName(), method.getParameterTypes()); Method equivalentMethod = iface.getMethod(method.getName(), method.getParameterTypes());
T result = searchWithFindSemantics(equivalentMethod, annotationType, annotationName, T result = searchWithFindSemantics(equivalentMethod, annotationType, annotationName, containerType,
processor, visited, metaDepth); processor, visited, metaDepth);
if (result != null) { if (result != null) {
return result; return result;
@ -1034,6 +1173,55 @@ public class AnnotatedElementUtils {
return null; return null;
} }
/**
* Get the array of raw (unsynthesized) annotations from the {@code value}
* attribute of the supplied repeatable annotation {@code container}.
* @since 4.3
*/
@SuppressWarnings("unchecked")
private static <A extends Annotation> A[] getRawAnnotationsFromContainer(AnnotatedElement element,
Annotation container) {
try {
return (A[]) AnnotationUtils.getValue(container);
}
catch (Exception ex) {
AnnotationUtils.handleIntrospectionFailure(element, ex);
}
// Unable to read value from repeating annotation container -> ignore it.
return (A[]) EMPTY_ANNOTATION_ARRAY;
}
/**
* Validate that the supplied {@code containerType} is a proper container
* annotation for the supplied repeatable {@code annotationType} (i.e.,
* that it declares a {@code value} attribute that holds an array of the
* {@code annotationType}).
* @since 4.3
* @throws AnnotationConfigurationException if the supplied {@code containerType}
* is not a valid container annotation for the supplied {@code annotationType}
*/
private static void validateRepeatableContainerType(Class<? extends Annotation> annotationType,
Class<? extends Annotation> containerType) {
try {
Method method = containerType.getDeclaredMethod(AnnotationUtils.VALUE);
Class<?> returnType = method.getReturnType();
if (!returnType.isArray() || returnType.getComponentType() != annotationType) {
String msg = String.format(
"Container type [%s] must declare a 'value' attribute for an array of type [%s]",
containerType.getName(), annotationType.getName());
throw new AnnotationConfigurationException(msg);
}
}
catch (Exception ex) {
AnnotationUtils.rethrowAnnotationConfigurationException(ex);
String msg = String.format("Invalid declaration of container type [%s] for repeatable annotation [%s]",
containerType.getName(), annotationType.getName());
throw new AnnotationConfigurationException(msg, ex);
}
}
/** /**
* Callback interface that is used to process annotations during a search. * Callback interface that is used to process annotations during a search.
@ -1114,10 +1302,10 @@ public class AnnotatedElementUtils {
/** /**
* Get the list of results aggregated by this processor. * Get the list of results aggregated by this processor.
* <p>NOTE: the processor does not aggregate the results itself. * <p>NOTE: the processor does <strong>not</strong> aggregate the results
* Rather, the search algorithm that uses this processor is responsible * itself. Rather, the search algorithm that uses this processor is
* for asking this processor if it {@link #aggregates} results and then * responsible for asking this processor if it {@link #aggregates} results
* adding the post-processed results to the list returned by this * and then adding the post-processed results to the list returned by this
* method. * method.
* <p>WARNING: aggregation is currently only supported for <em>find semantics</em>. * <p>WARNING: aggregation is currently only supported for <em>find semantics</em>.
* @return the list of results aggregated by this processor; never * @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 * target annotation during the {@link #process} phase and then merges
* annotation attributes from lower levels in the annotation hierarchy * annotation attributes from lower levels in the annotation hierarchy
* during the {@link #postProcess} phase. * during the {@link #postProcess} phase.
* <p>A {@code MergedAnnotationAttributesProcessor} may optionally be
* configured to {@linkplain #aggregates aggregate} results.
* @since 4.2 * @since 4.2
* @see AnnotationUtils#retrieveAnnotationAttributes * @see AnnotationUtils#retrieveAnnotationAttributes
* @see AnnotationUtils#postProcessAnnotationAttributes * @see AnnotationUtils#postProcessAnnotationAttributes
@ -1172,6 +1362,8 @@ public class AnnotatedElementUtils {
private final boolean nestedAnnotationsAsMap; private final boolean nestedAnnotationsAsMap;
private final boolean aggregates;
private final List<AnnotationAttributes> aggregatedResults; private final List<AnnotationAttributes> aggregatedResults;
@ -1188,12 +1380,13 @@ public class AnnotatedElementUtils {
this.annotationName = annotationName; this.annotationName = annotationName;
this.classValuesAsString = classValuesAsString; this.classValuesAsString = classValuesAsString;
this.nestedAnnotationsAsMap = nestedAnnotationsAsMap; this.nestedAnnotationsAsMap = nestedAnnotationsAsMap;
this.aggregates = aggregates;
this.aggregatedResults = (aggregates ? new ArrayList<AnnotationAttributes>() : null); this.aggregatedResults = (aggregates ? new ArrayList<AnnotationAttributes>() : null);
} }
@Override @Override
public boolean aggregates() { public boolean aggregates() {
return this.aggregatedResults != null; return this.aggregates;
} }
@Override @Override
@ -1232,7 +1425,7 @@ public class AnnotatedElementUtils {
targetAttributeNames.add(attributeOverrideName); targetAttributeNames.add(attributeOverrideName);
valuesAlreadyReplaced.add(attributeOverrideName); valuesAlreadyReplaced.add(attributeOverrideName);
// Ensure all aliased attributes in the target annotation are also overridden. (SPR-14069) // Ensure all aliased attributes in the target annotation are overridden. (SPR-14069)
List<String> aliases = AnnotationUtils.getAttributeAliasMap(targetAnnotationType).get(attributeOverrideName); List<String> aliases = AnnotationUtils.getAttributeAliasMap(targetAnnotationType).get(attributeOverrideName);
if (aliases != null) { if (aliases != null) {
for (String alias : aliases) { for (String alias : aliases) {

View File

@ -111,6 +111,7 @@ public abstract class AnnotationUtils {
*/ */
public static final String VALUE = "value"; public static final String VALUE = "value";
private static final String REPEATABLE_CLASS_NAME = "java.lang.annotation.Repeatable";
private static final Map<AnnotationCacheKey, Annotation> findAnnotationCache = private static final Map<AnnotationCacheKey, Annotation> findAnnotationCache =
new ConcurrentReferenceHashMap<AnnotationCacheKey, Annotation>(256); new ConcurrentReferenceHashMap<AnnotationCacheKey, Annotation>(256);
@ -1720,6 +1721,29 @@ public abstract class AnnotationUtils {
return (method != null && method.getName().equals("annotationType") && method.getParameterTypes().length == 0); return (method != null && method.getName().equals("annotationType") && method.getParameterTypes().length == 0);
} }
/**
* Resolve the container type for the supplied repeatable {@code annotationType}.
* <p>Automatically detects a <em>container annotation</em> 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;
}
/** /**
* <p>If the supplied throwable is an {@link AnnotationConfigurationException}, * <p>If the supplied throwable is an {@link AnnotationConfigurationException},
* it will be cast to an {@code AnnotationConfigurationException} and thrown, * it will be cast to an {@code AnnotationConfigurationException} and thrown,
@ -1806,8 +1830,6 @@ public abstract class AnnotationUtils {
private static class AnnotationCollector<A extends Annotation> { private static class AnnotationCollector<A extends Annotation> {
private static final String REPEATABLE_CLASS_NAME = "java.lang.annotation.Repeatable";
private final Class<A> annotationType; private final Class<A> annotationType;
private final Class<? extends Annotation> containerAnnotationType; private final Class<? extends Annotation> containerAnnotationType;
@ -1825,21 +1847,6 @@ public abstract class AnnotationUtils {
this.declaredMode = declaredMode; 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<A> getResult(AnnotatedElement element) { Set<A> getResult(AnnotatedElement element) {
process(element); process(element);
return Collections.unmodifiableSet(this.result); return Collections.unmodifiableSet(this.result);

View File

@ -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.
*
* <p>See <a href="https://jira.spring.io/browse/SPR-13973">SPR-13973</a>.
*
* @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);
Set<PeteRepeat> peteRepeats = findMergedRepeatableAnnotations(element, PeteRepeat.class);
assertNotNull(peteRepeats);
assertEquals(3, peteRepeats.size());
Iterator<PeteRepeat> iterator = peteRepeats.iterator();
assertEquals("A", iterator.next().value());
assertEquals("B", iterator.next().value());
assertEquals("C", iterator.next().value());
}
// -------------------------------------------------------------------------
@Retention(RetentionPolicy.RUNTIME)
@interface NonRepeatable {
}
@Retention(RetentionPolicy.RUNTIME)
@interface ContainerMissingValueAttribute {
// InvalidRepeatable[] value();
}
@Retention(RetentionPolicy.RUNTIME)
@interface ContainerWithNonArrayValueAttribute {
InvalidRepeatable value();
}
@Retention(RetentionPolicy.RUNTIME)
@interface ContainerWithArrayValueAttributeButWrongComponentType {
String[] value();
}
@Retention(RetentionPolicy.RUNTIME)
@interface InvalidRepeatable {
}
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface PeteRepeats {
PeteRepeat[] value();
}
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Repeatable(PeteRepeats.class)
@interface PeteRepeat {
String value();
}
@PeteRepeat("shadowed")
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface ForPetesSake {
@AliasFor(annotation = PeteRepeat.class)
String value();
}
@PeteRepeat("shadowed")
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface ForTheLoveOfFoo {
@AliasFor(annotation = PeteRepeat.class)
String value();
}
@PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") })
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface ComposedContainer {
}
@PeteRepeat("A")
@PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") })
static class RepeatableClass {
}
static class SubRepeatableClass extends RepeatableClass {
}
@ForPetesSake("B")
@ForTheLoveOfFoo("C")
@PeteRepeat("A")
static class ComposedRepeatableClass {
}
@ForPetesSake("C")
@PeteRepeats(@PeteRepeat("A"))
@PeteRepeat("B")
static class ComposedRepeatableMixedWithContainerClass {
}
@PeteRepeat("A")
@ComposedContainer
static class ComposedContainerClass {
}
}