Avoid annotation synthesizing for getAnnotationAttributes retrieval

Issue: SPR-13621
This commit is contained in:
Juergen Hoeller
2015-11-05 16:35:07 +01:00
parent 971f046913
commit 9683da52b2
3 changed files with 152 additions and 165 deletions

View File

@@ -977,7 +977,7 @@ public class AnnotatedElementUtils {
* annotation attributes from lower levels in the annotation hierarchy
* during the {@link #postProcess} phase.
* @since 4.2
* @see AnnotationUtils#getAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean, boolean)
* @see AnnotationUtils#retrieveAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean)
* @see AnnotationUtils#postProcessAnnotationAttributes
*/
private static class MergedAnnotationAttributesProcessor implements Processor<AnnotationAttributes> {
@@ -1003,8 +1003,8 @@ public class AnnotatedElementUtils {
public AnnotationAttributes process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) {
boolean found = (this.annotationType != null ? annotation.annotationType() == this.annotationType :
annotation.annotationType().getName().equals(this.annotationName));
return (found ? AnnotationUtils.getAnnotationAttributes(annotatedElement, annotation,
this.classValuesAsString, this.nestedAnnotationsAsMap, true) : null);
return (found ? AnnotationUtils.retrieveAnnotationAttributes(annotatedElement, annotation,
this.classValuesAsString, this.nestedAnnotationsAsMap) : null);
}
@Override

View File

@@ -25,8 +25,8 @@ import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -112,12 +112,6 @@ public abstract class AnnotationUtils {
public static final String VALUE = "value";
/**
* An object that can be stored in {@link AnnotationAttributes} as a
* placeholder for an attribute's declared default value.
*/
private static final Object DEFAULT_VALUE_PLACEHOLDER = new String("<SPRING DEFAULT VALUE PLACEHOLDER>");
private static final Map<AnnotationCacheKey, Annotation> findAnnotationCache =
new ConcurrentReferenceHashMap<AnnotationCacheKey, Annotation>(256);
@@ -1002,7 +996,10 @@ public abstract class AnnotationUtils {
public static AnnotationAttributes getAnnotationAttributes(AnnotatedElement annotatedElement,
Annotation annotation, boolean classValuesAsString, boolean nestedAnnotationsAsMap) {
return getAnnotationAttributes(annotatedElement, annotation, classValuesAsString, nestedAnnotationsAsMap, false);
AnnotationAttributes attributes =
retrieveAnnotationAttributes(annotatedElement, annotation, classValuesAsString, nestedAnnotationsAsMap);
postProcessAnnotationAttributes(annotatedElement, attributes, classValuesAsString, nestedAnnotationsAsMap);
return attributes;
}
/**
@@ -1010,16 +1007,9 @@ public abstract class AnnotationUtils {
* <p>This method provides fully recursive annotation reading capabilities on par with
* the reflection-based {@link org.springframework.core.type.StandardAnnotationMetadata}.
* <p><strong>NOTE</strong>: This variant of {@code getAnnotationAttributes()} is
* only intended for use within the framework. Specifically, the {@code mergeMode} flag
* can be set to {@code true} in order to support processing of attribute aliases while
* merging attributes within an annotation hierarchy. When running in <em>merge mode</em>,
* the following special rules apply:
* only intended for use within the framework. The following special rules apply:
* <ol>
* <li>The supplied annotation will <em>not</em> be
* {@linkplain #synthesizeAnnotation synthesized} before retrieving its attributes;
* however, nested annotations and arrays of nested annotations <em>will</em> be
* synthesized.</li>
* <li>Default values will be replaced with {@link #DEFAULT_VALUE_PLACEHOLDER}.</li>
* <li>Default values will be replaced with default value placeholders.</li>
* <li>The resulting, merged annotation attributes should eventually be
* {@linkplain #postProcessAnnotationAttributes post-processed} in order to
* ensure that placeholders have been replaced by actual default values and
@@ -1035,33 +1025,26 @@ public abstract class AnnotationUtils {
* {@link AnnotationAttributes} maps (for compatibility with
* {@link org.springframework.core.type.AnnotationMetadata}) or to preserve them as
* {@code Annotation} instances
* @param mergeMode whether the annotation attributes should be created
* using <em>merge mode</em>
* @return the annotation attributes (a specialized Map) with attribute names as keys
* and corresponding attribute values as values (never {@code null})
* @since 4.2
* @see #postProcessAnnotationAttributes
*/
static AnnotationAttributes getAnnotationAttributes(AnnotatedElement annotatedElement, Annotation annotation,
boolean classValuesAsString, boolean nestedAnnotationsAsMap, boolean mergeMode) {
if (!mergeMode) {
annotation = synthesizeAnnotation(annotation, annotatedElement);
}
static AnnotationAttributes retrieveAnnotationAttributes(AnnotatedElement annotatedElement, Annotation annotation,
boolean classValuesAsString, boolean nestedAnnotationsAsMap) {
Class<? extends Annotation> annotationType = annotation.annotationType();
AnnotationAttributes attrs = new AnnotationAttributes(annotationType);
AnnotationAttributes attributes = new AnnotationAttributes(annotationType);
for (Method method : getAttributeMethods(annotationType)) {
try {
Object value = method.invoke(annotation);
Object attributeValue = method.invoke(annotation);
Object defaultValue = method.getDefaultValue();
if (mergeMode && defaultValue != null) {
if (ObjectUtils.nullSafeEquals(value, defaultValue)) {
value = DEFAULT_VALUE_PLACEHOLDER;
}
if (defaultValue != null && ObjectUtils.nullSafeEquals(attributeValue, defaultValue)) {
attributeValue = new DefaultValueHolder(defaultValue);
}
attrs.put(method.getName(),
adaptValue(annotatedElement, value, classValuesAsString, nestedAnnotationsAsMap));
attributes.put(method.getName(),
adaptValue(annotatedElement, attributeValue, classValuesAsString, nestedAnnotationsAsMap));
}
catch (Exception ex) {
if (ex instanceof InvocationTargetException) {
@@ -1071,7 +1054,8 @@ public abstract class AnnotationUtils {
throw new IllegalStateException("Could not obtain annotation attribute value for " + method, ex);
}
}
return attrs;
return attributes;
}
/**
@@ -1109,7 +1093,6 @@ public abstract class AnnotationUtils {
if (value instanceof Annotation) {
Annotation annotation = (Annotation) value;
if (nestedAnnotationsAsMap) {
return getAnnotationAttributes(annotatedElement, annotation, classValuesAsString, true);
}
@@ -1120,12 +1103,11 @@ public abstract class AnnotationUtils {
if (value instanceof Annotation[]) {
Annotation[] annotations = (Annotation[]) value;
if (nestedAnnotationsAsMap) {
AnnotationAttributes[] mappedAnnotations = new AnnotationAttributes[annotations.length];
for (int i = 0; i < annotations.length; i++) {
mappedAnnotations[i] = getAnnotationAttributes(annotatedElement, annotations[i],
classValuesAsString, true);
mappedAnnotations[i] =
getAnnotationAttributes(annotatedElement, annotations[i], classValuesAsString, true);
}
return mappedAnnotations;
}
@@ -1138,6 +1120,101 @@ public abstract class AnnotationUtils {
return value;
}
/**
* Post-process the supplied {@link AnnotationAttributes}.
* <p>Specifically, this method enforces <em>attribute alias</em> semantics
* for annotation attributes that are annotated with {@link AliasFor @AliasFor}
* and replaces default value placeholders with their original default values.
* @param annotatedElement the element that is annotated with an annotation or
* annotation hierarchy from which the supplied attributes were created;
* may be {@code null} if unknown
* @param attributes the annotation attributes to post-process
* @param classValuesAsString whether to convert Class references into Strings (for
* compatibility with {@link org.springframework.core.type.AnnotationMetadata})
* or to preserve them as Class references
* @param nestedAnnotationsAsMap whether to convert nested annotations into
* {@link AnnotationAttributes} maps (for compatibility with
* {@link org.springframework.core.type.AnnotationMetadata}) or to preserve them as
* {@code Annotation} instances
* @since 4.2
* @see #retrieveAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean)
* @see #getDefaultValue(Class, String)
*/
static void postProcessAnnotationAttributes(AnnotatedElement annotatedElement,
AnnotationAttributes attributes, boolean classValuesAsString, boolean nestedAnnotationsAsMap) {
// Abort?
if (attributes == null) {
return;
}
Class<? extends Annotation> annotationType = attributes.annotationType();
// Track which attribute values have already been replaced so that we can short
// circuit the search algorithms.
Set<String> valuesAlreadyReplaced = new HashSet<String>();
// Validate @AliasFor configuration
Map<String, List<String>> aliasMap = getAttributeAliasMap(annotationType);
for (String attributeName : aliasMap.keySet()) {
if (valuesAlreadyReplaced.contains(attributeName)) {
continue;
}
Object value = attributes.get(attributeName);
boolean valuePresent = (value != null && !(value instanceof DefaultValueHolder));
for (String aliasedAttributeName : aliasMap.get(attributeName)) {
if (valuesAlreadyReplaced.contains(aliasedAttributeName)) {
continue;
}
Object aliasedValue = attributes.get(aliasedAttributeName);
boolean aliasPresent = (aliasedValue != null && !(aliasedValue instanceof DefaultValueHolder));
// Something to validate or replace with an alias?
if (valuePresent || aliasPresent) {
if (valuePresent && aliasPresent) {
// Since annotation attributes can be arrays, we must use ObjectUtils.nullSafeEquals().
if (!ObjectUtils.nullSafeEquals(value, aliasedValue)) {
String elementAsString = (annotatedElement != null ? annotatedElement.toString() : "unknown element");
throw new AnnotationConfigurationException(String.format(
"In AnnotationAttributes for annotation [%s] declared on %s, " +
"attribute '%s' and its alias '%s' are declared with values of [%s] and [%s], " +
"but only one is permitted.", annotationType.getName(), elementAsString,
attributeName, aliasedAttributeName, ObjectUtils.nullSafeToString(value),
ObjectUtils.nullSafeToString(aliasedValue)));
}
}
else if (aliasPresent) {
// Replace value with aliasedValue
attributes.put(attributeName,
adaptValue(annotatedElement, aliasedValue, classValuesAsString, nestedAnnotationsAsMap));
valuesAlreadyReplaced.add(attributeName);
}
else {
// Replace aliasedValue with value
attributes.put(aliasedAttributeName,
adaptValue(annotatedElement, value, classValuesAsString, nestedAnnotationsAsMap));
valuesAlreadyReplaced.add(aliasedAttributeName);
}
}
}
}
// Replace any remaining placeholders with actual default values
for (String attributeName : attributes.keySet()) {
if (valuesAlreadyReplaced.contains(attributeName)) {
continue;
}
Object value = attributes.get(attributeName);
if (value instanceof DefaultValueHolder) {
value = ((DefaultValueHolder) value).defaultValue;
attributes.put(attributeName,
adaptValue(annotatedElement, value, classValuesAsString, nestedAnnotationsAsMap));
}
}
}
/**
* Retrieve the <em>value</em> of the {@code value} attribute of a
* single-element Annotation, given an annotation instance.
@@ -1436,7 +1513,7 @@ public abstract class AnnotationUtils {
return map;
}
map = new HashMap<String, List<String>>();
map = new LinkedHashMap<String, List<String>>();
for (Method attribute : getAttributeMethods(annotationType)) {
List<String> aliasNames = getAttributeAliasNames(attribute);
if (!aliasNames.isEmpty()) {
@@ -1609,103 +1686,6 @@ public abstract class AnnotationUtils {
return (method != null && method.getName().equals("annotationType") && method.getParameterTypes().length == 0);
}
/**
* Post-process the supplied {@link AnnotationAttributes}.
* <p>Specifically, this method enforces <em>attribute alias</em> semantics
* for annotation attributes that are annotated with {@link AliasFor @AliasFor}
* and replaces {@linkplain #DEFAULT_VALUE_PLACEHOLDER placeholders} with their
* original default values.
* @param element the element that is annotated with an annotation or
* annotation hierarchy from which the supplied attributes were created;
* may be {@code null} if unknown
* @param attributes the annotation attributes to post-process
* @param classValuesAsString whether to convert Class references into Strings (for
* compatibility with {@link org.springframework.core.type.AnnotationMetadata})
* or to preserve them as Class references
* @param nestedAnnotationsAsMap whether to convert nested annotations into
* {@link AnnotationAttributes} maps (for compatibility with
* {@link org.springframework.core.type.AnnotationMetadata}) or to preserve them as
* {@code Annotation} instances
* @since 4.2
* @see #getAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean, boolean)
* @see #DEFAULT_VALUE_PLACEHOLDER
* @see #getDefaultValue(Class, String)
*/
static void postProcessAnnotationAttributes(AnnotatedElement element, AnnotationAttributes attributes,
boolean classValuesAsString, boolean nestedAnnotationsAsMap) {
// Abort?
if (attributes == null) {
return;
}
Class<? extends Annotation> annotationType = attributes.annotationType();
// Track which attribute values have already been replaced so that we can short
// circuit the search algorithms.
Set<String> valuesAlreadyReplaced = new HashSet<String>();
// Validate @AliasFor configuration
Map<String, List<String>> aliasMap = getAttributeAliasMap(annotationType);
for (String attributeName : aliasMap.keySet()) {
if (valuesAlreadyReplaced.contains(attributeName)) {
continue;
}
Object value = attributes.get(attributeName);
boolean valuePresent = (value != null && value != DEFAULT_VALUE_PLACEHOLDER);
for (String aliasedAttributeName : aliasMap.get(attributeName)) {
if (valuesAlreadyReplaced.contains(aliasedAttributeName)) {
continue;
}
Object aliasedValue = attributes.get(aliasedAttributeName);
boolean aliasPresent = (aliasedValue != null && aliasedValue != DEFAULT_VALUE_PLACEHOLDER);
// Something to validate or replace with an alias?
if (valuePresent || aliasPresent) {
if (valuePresent && aliasPresent) {
// Since annotation attributes can be arrays, we must use ObjectUtils.nullSafeEquals().
if (!ObjectUtils.nullSafeEquals(value, aliasedValue)) {
String elementAsString = (element != null ? element.toString() : "unknown element");
String msg = String.format("In AnnotationAttributes for annotation [%s] declared on [%s], " +
"attribute [%s] and its alias [%s] are declared with values of [%s] and [%s], " +
"but only one declaration is permitted.", annotationType.getName(),
elementAsString, attributeName, aliasedAttributeName,
ObjectUtils.nullSafeToString(value), ObjectUtils.nullSafeToString(aliasedValue));
throw new AnnotationConfigurationException(msg);
}
}
else if (aliasPresent) {
// Replace value with aliasedValue
attributes.put(attributeName,
adaptValue(element, aliasedValue, classValuesAsString, nestedAnnotationsAsMap));
valuesAlreadyReplaced.add(attributeName);
}
else {
// Replace aliasedValue with value
attributes.put(aliasedAttributeName,
adaptValue(element, value, classValuesAsString, nestedAnnotationsAsMap));
valuesAlreadyReplaced.add(aliasedAttributeName);
}
}
}
}
// Replace any remaining placeholders with actual default values
for (String attributeName : attributes.keySet()) {
if (valuesAlreadyReplaced.contains(attributeName)) {
continue;
}
Object value = attributes.get(attributeName);
if (value == DEFAULT_VALUE_PLACEHOLDER) {
attributes.put(attributeName,
adaptValue(element, getDefaultValue(annotationType, attributeName), classValuesAsString,
nestedAnnotationsAsMap));
}
}
}
/**
* <p>If the supplied throwable is an {@link AnnotationConfigurationException},
* it will be cast to an {@code AnnotationConfigurationException} and thrown,
@@ -2163,4 +2143,14 @@ public abstract class AnnotationUtils {
}
}
private static class DefaultValueHolder {
final Object defaultValue;
public DefaultValueHolder(Object defaultValue) {
this.defaultValue = defaultValue;
}
}
}