diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java index b07f67901b..f96fd6219c 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java @@ -30,7 +30,6 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.annotation.AnnotationAttributes; -import org.springframework.core.annotation.AnnotationConfigurationException; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.type.filter.AbstractTypeHierarchyTraversingFilter; @@ -41,7 +40,6 @@ import org.springframework.core.type.filter.RegexPatternTypeFilter; import org.springframework.core.type.filter.TypeFilter; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -117,7 +115,8 @@ class ComponentScanAnnotationParser { } Set basePackages = new LinkedHashSet(); - for (String pkg : getBasePackages(componentScan, declaringClass)) { + String[] basePackagesArray = componentScan.getAliasedStringArray("basePackages", ComponentScan.class, declaringClass); + for (String pkg : basePackagesArray) { String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg), ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); basePackages.addAll(Arrays.asList(tokenized)); @@ -138,25 +137,6 @@ class ComponentScanAnnotationParser { return scanner.doScan(StringUtils.toStringArray(basePackages)); } - private String[] getBasePackages(AnnotationAttributes componentScan, String declaringClass) { - String[] value = componentScan.getStringArray("value"); - String[] basePackages = componentScan.getStringArray("basePackages"); - boolean valueDeclared = !ObjectUtils.isEmpty(value); - boolean basePackagesDeclared = !ObjectUtils.isEmpty(basePackages); - - if (valueDeclared && basePackagesDeclared && !ObjectUtils.nullSafeEquals(value, basePackages)) { - String msg = String.format("In @ComponentScan declared on [%s], attribute [value] " - + "and its alias [basePackages] are present with values of [%s] and [%s], " - + "but only one is permitted.", declaringClass, ObjectUtils.nullSafeToString(value), - ObjectUtils.nullSafeToString(basePackages)); - throw new AnnotationConfigurationException(msg); - } - if (!basePackagesDeclared) { - basePackages = value; - } - return basePackages; - } - private List typeFiltersFor(AnnotationAttributes filterAttributes) { List typeFilters = new ArrayList(); FilterType filterType = filterAttributes.getEnum("type"); 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 c23bc12666..4747b22036 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 @@ -17,12 +17,14 @@ package org.springframework.core.annotation; import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Array; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -131,6 +133,58 @@ public class AnnotationAttributes extends LinkedHashMap { return doGet(attributeName, String[].class); } + /** + * Get the value stored under the specified {@code attributeName} as an + * array of strings, 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 array of strings + * @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 + */ + public String[] getAliasedStringArray(String attributeName, Class annotationType, + Object annotationSource) { + + Assert.hasText(attributeName, "attributeName must not be null or empty"); + Assert.notNull(annotationType, "annotationType must not be null"); + + String[] attributeValue = getStringArrayWithoutNullCheck(attributeName); + String aliasName = AnnotationUtils.getAttributeAliasMap(annotationType).get(attributeName); + String[] aliasValue = getStringArrayWithoutNullCheck(aliasName); + 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, + ObjectUtils.nullSafeToString(attributeValue), ObjectUtils.nullSafeToString(aliasValue)); + throw new AnnotationConfigurationException(msg); + } + + if (!attributeDeclared) { + attributeValue = aliasValue; + } + + assertAttributePresence(attributeName, aliasName, attributeValue); + + return attributeValue; + } + /** * Get the value stored under the specified {@code attributeName} as a * boolean. @@ -284,29 +338,52 @@ public class AnnotationAttributes extends LinkedHashMap { * @return the value * @throws IllegalArgumentException if the attribute does not exist or * if it is not of the expected type - * @since 4.2 */ @SuppressWarnings("unchecked") private T doGet(String attributeName, Class expectedType) { Assert.hasText(attributeName, "attributeName must not be null or empty"); Object value = get(attributeName); - if (value == null) { + 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( "Attribute '%s' not found in attributes for annotation [%s]", attributeName, this.displayName)); } - if (!expectedType.isInstance(value)) { - if (expectedType.isArray() && expectedType.getComponentType().isInstance(value)) { - Object array = Array.newInstance(expectedType.getComponentType(), 1); - Array.set(array, 0, value); - value = array; - } - else { - throw new IllegalArgumentException(String.format( - "Attribute '%s' is of type [%s], but [%s] was expected in attributes for annotation [%s]", - attributeName, value.getClass().getSimpleName(), expectedType.getSimpleName(), this.displayName)); - } + } + + private void assertAttributePresence(String attributeName, String aliasName, Object attributeValue) { + if (attributeValue == null) { + throw new IllegalArgumentException(String.format( + "Neither attribute '%s' nor its alias '%s' was found in attributes for annotation [%s]", attributeName, + aliasName, this.displayName)); } - return (T) value; + } + + private void assertAttributeType(String attributeName, Object attributeValue, Class expectedType) { + if (!expectedType.isInstance(attributeValue)) { + throw new IllegalArgumentException(String.format( + "Attribute '%s' is of type [%s], but [%s] was expected in attributes for annotation [%s]", + attributeName, attributeValue.getClass().getSimpleName(), expectedType.getSimpleName(), + this.displayName)); + } + } + + private String[] getStringArrayWithoutNullCheck(String attributeName) { + Object value = get(attributeName); + if (value != null) { + assertAttributeType(attributeName, value, String[].class); + } + return (String[]) value; } /** @@ -354,7 +431,6 @@ public class AnnotationAttributes extends LinkedHashMap { return String.valueOf(value); } - /** * Return an {@link AnnotationAttributes} instance based on the given map. *

If the map is already an {@code AnnotationAttributes} instance, it 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 b6ce14b296..4f90306dcf 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,71 @@ public class AnnotationAttributesTests { attributes.getEnum("color"); } + @Test + public void getAliasedStringArray() { + final String[] INPUT = new String[] { "test.xml" }; + final String[] EMPTY = new String[0]; + + attributes.clear(); + attributes.put("locations", INPUT); + assertArrayEquals(INPUT, getAliasedStringArray("locations")); + assertArrayEquals(INPUT, getAliasedStringArray("value")); + + attributes.clear(); + attributes.put("value", INPUT); + assertArrayEquals(INPUT, getAliasedStringArray("locations")); + assertArrayEquals(INPUT, getAliasedStringArray("value")); + + attributes.clear(); + attributes.put("locations", INPUT); + attributes.put("value", INPUT); + assertArrayEquals(INPUT, getAliasedStringArray("locations")); + assertArrayEquals(INPUT, getAliasedStringArray("value")); + + attributes.clear(); + attributes.put("locations", INPUT); + attributes.put("value", EMPTY); + assertArrayEquals(INPUT, getAliasedStringArray("locations")); + assertArrayEquals(INPUT, getAliasedStringArray("value")); + + attributes.clear(); + attributes.put("locations", EMPTY); + attributes.put("value", INPUT); + assertArrayEquals(INPUT, getAliasedStringArray("locations")); + assertArrayEquals(INPUT, getAliasedStringArray("value")); + + attributes.clear(); + attributes.put("locations", EMPTY); + attributes.put("value", EMPTY); + assertArrayEquals(EMPTY, getAliasedStringArray("locations")); + assertArrayEquals(EMPTY, getAliasedStringArray("value")); + } + + @Test + public void getAliasedStringArrayWithMissingAliasedAttributes() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage(equalTo("Neither attribute 'locations' nor its alias 'value' was found in attributes for annotation [unknown]")); + getAliasedStringArray("locations"); + } + + @Test + public void getAliasedStringArrayWithDifferentAliasedValues() { + attributes.put("locations", new String[] { "1.xml" }); + attributes.put("value", new String[] { "2.xml" }); + + exception.expect(AnnotationConfigurationException.class); + exception.expectMessage(containsString("In annotation [unknown]")); + 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")); + + getAliasedStringArray("locations"); + } + + private String[] getAliasedStringArray(String attributeName) { + return attributes.getAliasedStringArray(attributeName, ContextConfig.class, null); + } + enum Color { RED, WHITE, BLUE @@ -157,4 +222,18 @@ public class AnnotationAttributesTests { @Filter(pattern = "foo") static class FilteredClass { } + + /** + * Mock of {@code org.springframework.test.context.ContextConfiguration}. + */ + @Retention(RetentionPolicy.RUNTIME) + @interface ContextConfig { + + @AliasFor(attribute = "locations") + String value() default ""; + + @AliasFor(attribute = "value") + String locations() default ""; + } + }