diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java
index 3990916dc69..1ab67ab6afd 100644
--- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java
+++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java
@@ -29,17 +29,21 @@ import org.springframework.util.StringUtils;
/**
* {@link LinkedHashMap} subclass representing annotation attribute
- * key-value pairs as read by Spring's reflection- or ASM-based
- * {@link org.springframework.core.type.AnnotationMetadata} implementations,
- * {@link AnnotationUtils}, and {@link AnnotatedElementUtils}.
+ * key-value pairs as read by {@link AnnotationUtils},
+ * {@link AnnotatedElementUtils}, and Spring's reflection- and ASM-based
+ * {@link org.springframework.core.type.AnnotationMetadata} implementations.
*
*
Provides 'pseudo-reification' to avoid noisy Map generics in the calling
* code as well as convenience methods for looking up annotation attributes
- * in a type-safe fashion.
+ * in a type-safe fashion, including support for attribute aliases configured
+ * via {@link AliasFor @AliasFor}.
*
* @author Chris Beams
* @author Sam Brannen
* @since 3.1.1
+ * @see AnnotationUtils#getAnnotationAttributes
+ * @see AnnotatedElementUtils
+ * @see AliasFor
*/
@SuppressWarnings("serial")
public class AnnotationAttributes extends LinkedHashMap {
@@ -117,6 +121,34 @@ public class AnnotationAttributes extends LinkedHashMap {
return getRequiredAttribute(attributeName, String.class);
}
+ /**
+ * Get the value stored under the specified {@code attributeName} as a
+ * string, taking into account alias semantics defined via
+ * {@link AliasFor @AliasFor}.
+ * If there is no value stored under the specified {@code attributeName}
+ * but the attribute has an alias declared via {@code @AliasFor}, the
+ * value of the alias will be returned.
+ *
+ * @param attributeName the name of the attribute to get; never
+ * {@code null} or empty
+ * @param annotationType the type of annotation represented by this
+ * {@code AnnotationAttributes} instance; never {@code null}
+ * @param annotationSource the source of the annotation represented by
+ * this {@code AnnotationAttributes} (e.g., the {@link AnnotatedElement});
+ * or {@code null} if unknown
+ * @return the string value
+ * @throws IllegalArgumentException if the attribute and its alias do
+ * not exist or are not of type {@code String}
+ * @throws AnnotationConfigurationException if the attribute and its
+ * alias are both present with different non-empty values
+ * @since 4.2
+ * @see ObjectUtils#isEmpty(Object)
+ */
+ public String getAliasedString(String attributeName, Class extends Annotation> annotationType,
+ Object annotationSource) {
+ return getRequiredAttributeWithAlias(attributeName, annotationType, annotationSource, String.class);
+ }
+
/**
* Get the value stored under the specified {@code attributeName} as an
* array of strings.
@@ -157,7 +189,7 @@ public class AnnotationAttributes extends LinkedHashMap {
*/
public String[] getAliasedStringArray(String attributeName, Class extends Annotation> annotationType,
Object annotationSource) {
- return getRequiredArrayWithAttributeAlias(attributeName, annotationType, annotationSource, String[].class);
+ return getRequiredAttributeWithAlias(attributeName, annotationType, annotationSource, String[].class);
}
/**
@@ -255,7 +287,7 @@ public class AnnotationAttributes extends LinkedHashMap {
*/
public Class>[] getAliasedClassArray(String attributeName, Class extends Annotation> annotationType,
Object annotationSource) {
- return getRequiredArrayWithAttributeAlias(attributeName, annotationType, annotationSource, Class[].class);
+ return getRequiredAttributeWithAlias(attributeName, annotationType, annotationSource, Class[].class);
}
/**
@@ -326,9 +358,39 @@ public class AnnotationAttributes extends LinkedHashMap {
return (A[]) getRequiredAttribute(attributeName, array.getClass());
}
+ /**
+ * Get the value stored under the specified {@code attributeName},
+ * ensuring that the value is of the {@code expectedType}.
+ * If the {@code expectedType} is an array and the value stored
+ * under the specified {@code attributeName} is a single element of the
+ * component type of the expected array type, the single element will be
+ * wrapped in a single-element array of the appropriate type before
+ * returning it.
+ * @param attributeName the name of the attribute to get; never
+ * {@code null} or empty
+ * @param expectedType the expected type; never {@code null}
+ * @return the value
+ * @throws IllegalArgumentException if the attribute does not exist or
+ * if it is not of the expected type
+ */
+ @SuppressWarnings("unchecked")
+ private T getRequiredAttribute(String attributeName, Class expectedType) {
+ Assert.hasText(attributeName, "attributeName must not be null or empty");
+ Object value = get(attributeName);
+ assertAttributePresence(attributeName, value);
+ if (!expectedType.isInstance(value) && expectedType.isArray()
+ && expectedType.getComponentType().isInstance(value)) {
+ Object array = Array.newInstance(expectedType.getComponentType(), 1);
+ Array.set(array, 0, value);
+ value = array;
+ }
+ assertAttributeType(attributeName, value, expectedType);
+ return (T) value;
+ }
+
/**
* Get the value stored under the specified {@code attributeName} as an
- * array of the {@code expectedType}, taking into account alias semantics
+ * object of the {@code expectedType}, taking into account alias semantics
* defined via {@link AliasFor @AliasFor}.
* If there is no value stored under the specified {@code attributeName}
* but the attribute has an alias declared via {@code @AliasFor}, the
@@ -341,34 +403,33 @@ public class AnnotationAttributes extends LinkedHashMap {
* @param annotationSource the source of the annotation represented by
* this {@code AnnotationAttributes} (e.g., the {@link AnnotatedElement});
* or {@code null} if unknown
- * @param expectedType the expected array type; never {@code null}
- * @return the array of values
+ * @param expectedType the expected type; never {@code null}
+ * @return the value
* @throws IllegalArgumentException if the attribute and its alias do
- * not exist or are not of the {@code expectedType}, or if the
- * {@code expectedType} is not an array
+ * not exist or are not of the {@code expectedType}
* @throws AnnotationConfigurationException if the attribute and its
* alias are both present with different non-empty values
* @since 4.2
+ * @see ObjectUtils#isEmpty(Object)
*/
- private T getRequiredArrayWithAttributeAlias(String attributeName, Class extends Annotation> annotationType,
+ private T getRequiredAttributeWithAlias(String attributeName, Class extends Annotation> annotationType,
Object annotationSource, Class expectedType) {
Assert.hasText(attributeName, "attributeName must not be null or empty");
Assert.notNull(annotationType, "annotationType must not be null");
Assert.notNull(expectedType, "expectedType must not be null");
- Assert.isTrue(expectedType.isArray(), "expectedType must be an array");
T attributeValue = getAttribute(attributeName, expectedType);
String aliasName = AnnotationUtils.getAttributeAliasMap(annotationType).get(attributeName);
T aliasValue = getAttribute(aliasName, expectedType);
- boolean attributeDeclared = !ObjectUtils.isEmpty((Object[]) attributeValue);
- boolean aliasDeclared = !ObjectUtils.isEmpty((Object[]) aliasValue);
+ boolean attributeDeclared = !ObjectUtils.isEmpty(attributeValue);
+ boolean aliasDeclared = !ObjectUtils.isEmpty(aliasValue);
if (!ObjectUtils.nullSafeEquals(attributeValue, aliasValue) && attributeDeclared && aliasDeclared) {
String elementName = (annotationSource == null ? "unknown element" : annotationSource.toString());
String msg = String.format("In annotation [%s] declared on [%s], "
+ "attribute [%s] and its alias [%s] are present with values of [%s] and [%s], "
- + "but only one is permitted.", this.displayName, elementName, attributeName, aliasName,
+ + "but only one is permitted.", annotationType.getName(), elementName, attributeName, aliasName,
ObjectUtils.nullSafeToString(attributeValue), ObjectUtils.nullSafeToString(aliasValue));
throw new AnnotationConfigurationException(msg);
}
@@ -402,36 +463,6 @@ public class AnnotationAttributes extends LinkedHashMap {
return (T) value;
}
- /**
- * Get the value stored under the specified {@code attributeName},
- * ensuring that the value is of the {@code expectedType}.
- * If the {@code expectedType} is an array and the value stored
- * under the specified {@code attributeName} is a single element of the
- * component type of the expected array type, the single element will be
- * wrapped in a single-element array of the appropriate type before
- * returning it.
- * @param attributeName the name of the attribute to get; never
- * {@code null} or empty
- * @param expectedType the expected type; never {@code null}
- * @return the value
- * @throws IllegalArgumentException if the attribute does not exist or
- * if it is not of the expected type
- */
- @SuppressWarnings("unchecked")
- private T getRequiredAttribute(String attributeName, Class expectedType) {
- Assert.hasText(attributeName, "attributeName must not be null or empty");
- Object value = get(attributeName);
- assertAttributePresence(attributeName, value);
- if (!expectedType.isInstance(value) && expectedType.isArray()
- && expectedType.getComponentType().isInstance(value)) {
- Object array = Array.newInstance(expectedType.getComponentType(), 1);
- Array.set(array, 0, value);
- value = array;
- }
- assertAttributeType(attributeName, value, expectedType);
- return (T) value;
- }
-
private void assertAttributePresence(String attributeName, Object attributeValue) {
if (attributeValue == null) {
throw new IllegalArgumentException(String.format(
diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java
index 4de1c2a311a..15eaec6b309 100644
--- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java
+++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java
@@ -144,6 +144,63 @@ public class AnnotationAttributesTests {
attributes.getEnum("color");
}
+ @Test
+ public void getAliasedString() {
+ attributes.clear();
+ attributes.put("name", "metaverse");
+ assertEquals("metaverse", getAliasedString("name"));
+ assertEquals("metaverse", getAliasedString("value"));
+
+ attributes.clear();
+ attributes.put("value", "metaverse");
+ assertEquals("metaverse", getAliasedString("name"));
+ assertEquals("metaverse", getAliasedString("value"));
+
+ attributes.clear();
+ attributes.put("name", "metaverse");
+ attributes.put("value", "metaverse");
+ assertEquals("metaverse", getAliasedString("name"));
+ assertEquals("metaverse", getAliasedString("value"));
+ }
+
+ @Test
+ public void getAliasedStringFromSynthesizedAnnotationAttributes() {
+ Scope scope = ScopedComponent.class.getAnnotation(Scope.class);
+ AnnotationAttributes scopeAttributes = AnnotationUtils.getAnnotationAttributes(ScopedComponent.class, scope);
+
+ assertEquals("custom", getAliasedString(scopeAttributes, "name"));
+ assertEquals("custom", getAliasedString(scopeAttributes, "value"));
+ }
+
+ @Test
+ public void getAliasedStringWithMissingAliasedAttributes() {
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage(equalTo("Neither attribute 'name' nor its alias 'value' was found in attributes for annotation [unknown]"));
+ getAliasedString("name");
+ }
+
+ @Test
+ public void getAliasedStringWithDifferentAliasedValues() {
+ attributes.put("name", "request");
+ attributes.put("value", "session");
+
+ exception.expect(AnnotationConfigurationException.class);
+ exception.expectMessage(containsString("In annotation [" + Scope.class.getName() + "]"));
+ exception.expectMessage(containsString("attribute [name] and its alias [value]"));
+ exception.expectMessage(containsString("[request] and [session]"));
+ exception.expectMessage(containsString("but only one is permitted"));
+
+ getAliasedString("name");
+ }
+
+ private String getAliasedString(String attributeName) {
+ return getAliasedString(this.attributes, attributeName);
+ }
+
+ private String getAliasedString(AnnotationAttributes attrs, String attributeName) {
+ return attrs.getAliasedString(attributeName, Scope.class, null);
+ }
+
@Test
public void getAliasedStringArray() {
final String[] INPUT = new String[] { "test.xml" };
@@ -197,7 +254,7 @@ public class AnnotationAttributesTests {
attributes.put("value", new String[] { "2.xml" });
exception.expect(AnnotationConfigurationException.class);
- exception.expectMessage(containsString("In annotation [unknown]"));
+ exception.expectMessage(containsString("In annotation [" + ContextConfig.class.getName() + "]"));
exception.expectMessage(containsString("attribute [locations] and its alias [value]"));
exception.expectMessage(containsString("[{1.xml}] and [{2.xml}]"));
exception.expectMessage(containsString("but only one is permitted"));
@@ -262,7 +319,7 @@ public class AnnotationAttributesTests {
attributes.put("value", new Class[] { Number.class });
exception.expect(AnnotationConfigurationException.class);
- exception.expectMessage(containsString("In annotation [unknown]"));
+ exception.expectMessage(containsString("In annotation [" + Filter.class.getName() + "]"));
exception.expectMessage(containsString("attribute [classes] and its alias [value]"));
exception.expectMessage(containsString("[{class java.lang.String}] and [{class java.lang.Number}]"));
exception.expectMessage(containsString("but only one is permitted"));
@@ -308,4 +365,21 @@ public class AnnotationAttributesTests {
String locations() default "";
}
+ /**
+ * Mock of {@code org.springframework.context.annotation.Scope}.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface Scope {
+
+ @AliasFor(attribute = "name")
+ String value() default "singleton";
+
+ @AliasFor(attribute = "value")
+ String name() default "singleton";
+ }
+
+ @Scope(name = "custom")
+ static class ScopedComponent {
+ }
+
}