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 0f9749747d..9560a8451c 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 @@ -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 { @@ -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 diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java index 0fb02550ae..c504db73a2 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -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(""); - private static final Map findAnnotationCache = new ConcurrentReferenceHashMap(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 { *

This method provides fully recursive annotation reading capabilities on par with * the reflection-based {@link org.springframework.core.type.StandardAnnotationMetadata}. *

NOTE: 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 merge mode, - * the following special rules apply: + * only intended for use within the framework. The following special rules apply: *

    - *
  1. The supplied annotation will not be - * {@linkplain #synthesizeAnnotation synthesized} before retrieving its attributes; - * however, nested annotations and arrays of nested annotations will be - * synthesized.
  2. - *
  3. Default values will be replaced with {@link #DEFAULT_VALUE_PLACEHOLDER}.
  4. + *
  5. Default values will be replaced with default value placeholders.
  6. *
  7. 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 merge mode * @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 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}. + *

    Specifically, this method enforces attribute alias 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 annotationType = attributes.annotationType(); + + // Track which attribute values have already been replaced so that we can short + // circuit the search algorithms. + Set valuesAlreadyReplaced = new HashSet(); + + // Validate @AliasFor configuration + Map> 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 value 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>(); + map = new LinkedHashMap>(); for (Method attribute : getAttributeMethods(annotationType)) { List 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}. - *

    Specifically, this method enforces attribute alias 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 annotationType = attributes.annotationType(); - - // Track which attribute values have already been replaced so that we can short - // circuit the search algorithms. - Set valuesAlreadyReplaced = new HashSet(); - - // Validate @AliasFor configuration - Map> 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)); - } - } - } - /** *

    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; + } + } + } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java index 852de13c2e..465104b0e5 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java @@ -94,8 +94,7 @@ public class AnnotatedElementUtilsTests { public void hasMetaAnnotationTypesOnClassWithMetaDepth2() { assertTrue(hasMetaAnnotationTypes(ComposedTransactionalComponentClass.class, TX_NAME)); assertTrue(hasMetaAnnotationTypes(ComposedTransactionalComponentClass.class, Component.class.getName())); - assertFalse(hasMetaAnnotationTypes(ComposedTransactionalComponentClass.class, - ComposedTransactionalComponent.class.getName())); + assertFalse(hasMetaAnnotationTypes(ComposedTransactionalComponentClass.class, ComposedTransactionalComponent.class.getName())); } @Test @@ -111,7 +110,7 @@ public class AnnotatedElementUtilsTests { @Test public void isAnnotatedOnSubclassWithMetaDepth0() { assertFalse("isAnnotated() does not search the class hierarchy.", - isAnnotated(SubTransactionalComponentClass.class, TransactionalComponent.class.getName())); + isAnnotated(SubTransactionalComponentClass.class, TransactionalComponent.class.getName())); } @Test @@ -124,8 +123,7 @@ public class AnnotatedElementUtilsTests { public void isAnnotatedOnClassWithMetaDepth2() { assertTrue(isAnnotated(ComposedTransactionalComponentClass.class, TX_NAME)); assertTrue(isAnnotated(ComposedTransactionalComponentClass.class, Component.class.getName())); - assertTrue(isAnnotated(ComposedTransactionalComponentClass.class, - ComposedTransactionalComponent.class.getName())); + assertTrue(isAnnotated(ComposedTransactionalComponentClass.class, ComposedTransactionalComponent.class.getName())); } @Test @@ -167,7 +165,6 @@ public class AnnotatedElementUtilsTests { * type within the class hierarchy. Such undesirable behavior would cause the * logic in {@link org.springframework.context.annotation.ProfileCondition} * to fail. - * * @see org.springframework.core.env.EnvironmentSystemIntegrationTests#mostSpecificDerivedClassDrivesEnvironment_withDevEnvAndDerivedDevConfigClass */ @Test @@ -179,7 +176,6 @@ public class AnnotatedElementUtilsTests { /** * Note: this functionality is required by {@link org.springframework.context.annotation.ProfileCondition}. - * * @see org.springframework.core.env.EnvironmentSystemIntegrationTests */ @Test @@ -187,7 +183,7 @@ public class AnnotatedElementUtilsTests { MultiValueMap attributes = getAllAnnotationAttributes(TxFromMultipleComposedAnnotations.class, TX_NAME); assertNotNull("Annotation attributes map for @Transactional on TxFromMultipleComposedAnnotations", attributes); assertEquals("value for TxFromMultipleComposedAnnotations.", asList("TxInheritedComposed", "TxComposed"), - attributes.get("value")); + attributes.get("value")); } @Test @@ -419,12 +415,12 @@ public class AnnotatedElementUtilsTests { public void getMergedAnnotationAttributesWithInvalidConventionBasedComposedAnnotation() { Class element = InvalidConventionBasedComposedContextConfigClass.class; exception.expect(AnnotationConfigurationException.class); - exception.expectMessage(either(containsString("attribute [value] and its alias [locations]")).or( - containsString("attribute [locations] and its alias [value]"))); + exception.expectMessage(either(containsString("attribute 'value' and its alias 'locations'")).or( + containsString("attribute 'locations' and its alias 'value'"))); exception.expectMessage(either( - containsString("values of [{duplicateDeclaration}] and [{requiredLocationsDeclaration}]")).or( - containsString("values of [{requiredLocationsDeclaration}] and [{duplicateDeclaration}]"))); - exception.expectMessage(containsString("but only one declaration is permitted")); + containsString("values of [{duplicateDeclaration}] and [{requiredLocationsDeclaration}]")).or( + containsString("values of [{requiredLocationsDeclaration}] and [{duplicateDeclaration}]"))); + exception.expectMessage(containsString("but only one is permitted")); getMergedAnnotationAttributes(element, ContextConfig.class); } @@ -432,11 +428,11 @@ public class AnnotatedElementUtilsTests { public void getMergedAnnotationAttributesWithInvalidAliasedComposedAnnotation() { Class element = InvalidAliasedComposedContextConfigClass.class; exception.expect(AnnotationConfigurationException.class); - exception.expectMessage(either(containsString("attribute [value] and its alias [locations]")).or( - containsString("attribute [locations] and its alias [value]"))); + exception.expectMessage(either(containsString("attribute 'value' and its alias 'locations'")).or( + containsString("attribute 'locations' and its alias 'value'"))); exception.expectMessage(either(containsString("values of [{duplicateDeclaration}] and [{test.xml}]")).or( - containsString("values of [{test.xml}] and [{duplicateDeclaration}]"))); - exception.expectMessage(containsString("but only one declaration is permitted")); + containsString("values of [{test.xml}] and [{duplicateDeclaration}]"))); + exception.expectMessage(containsString("but only one is permitted")); getMergedAnnotationAttributes(element, ContextConfig.class); } @@ -493,11 +489,9 @@ public class AnnotatedElementUtilsTests { /** *

    {@code AbstractClassWithInheritedAnnotation} declares {@code handleParameterized(T)}; whereas, * {@code ConcreteClassWithInheritedAnnotation} declares {@code handleParameterized(String)}. - * - *

    As of Spring 4.2 RC1, {@code AnnotatedElementUtils.processWithFindSemantics()} does not resolve an + *

    As of Spring 4.2, {@code AnnotatedElementUtils.processWithFindSemantics()} does not resolve an * equivalent method in {@code AbstractClassWithInheritedAnnotation} for the bridged * {@code handleParameterized(String)} method. - * * @since 4.2 */ @Test @@ -517,6 +511,7 @@ public class AnnotatedElementUtilsTests { Method[] methods = StringGenericParameter.class.getMethods(); Method bridgeMethod = null; Method bridgedMethod = null; + for (Method method : methods) { if ("getFor".equals(method.getName()) && !method.getParameterTypes()[0].equals(Integer.class)) { if (method.getReturnType().equals(Object.class)) { @@ -546,13 +541,13 @@ public class AnnotatedElementUtilsTests { String qualifier = "aliasForQualifier"; // 1) Find and merge AnnotationAttributes from the annotation hierarchy - AnnotationAttributes attributes = findMergedAnnotationAttributes(AliasedTransactionalComponentClass.class, - AliasedTransactional.class); + AnnotationAttributes attributes = findMergedAnnotationAttributes( + AliasedTransactionalComponentClass.class, AliasedTransactional.class); assertNotNull("@AliasedTransactional on AliasedTransactionalComponentClass.", attributes); // 2) Synthesize the AnnotationAttributes back into the target annotation AliasedTransactional annotation = AnnotationUtils.synthesizeAnnotation(attributes, - AliasedTransactional.class, AliasedTransactionalComponentClass.class); + AliasedTransactional.class, AliasedTransactionalComponentClass.class); assertNotNull(annotation); // 3) Verify that the AnnotationAttributes and synthesized annotation are equivalent @@ -573,8 +568,8 @@ public class AnnotatedElementUtilsTests { @Test public void findMergedAnnotationForMultipleMetaAnnotationsWithClashingAttributeNames() { - final String[] xmlLocations = asArray("test.xml"); - final String[] propFiles = asArray("test.properties"); + String[] xmlLocations = asArray("test.xml"); + String[] propFiles = asArray("test.properties"); Class element = AliasedComposedContextConfigAndTestPropSourceClass.class; @@ -600,6 +595,7 @@ public class AnnotatedElementUtilsTests { String[] expected = asArray("com.example.app.test"); Class element = TestComponentScanClass.class; AnnotationAttributes attributes = findMergedAnnotationAttributes(element, ComponentScan.class); + assertNotNull("Should find @ComponentScan on " + element, attributes); assertArrayEquals("basePackages for " + element, expected, attributes.getStringArray("basePackages")); @@ -628,9 +624,10 @@ public class AnnotatedElementUtilsTests { @Test public void findMergedAnnotationWithLocalAliasesThatConflictWithAttributesInMetaAnnotationByConvention() { - final String[] EMPTY = new String[] {}; + final String[] EMPTY = new String[0]; Class element = SpringAppConfigClass.class; ContextConfig contextConfig = findMergedAnnotation(element, ContextConfig.class); + assertNotNull("Should find @ContextConfig on " + element, contextConfig); assertArrayEquals("locations for " + element, EMPTY, contextConfig.locations()); // 'value' in @SpringAppConfig should not override 'value' in @ContextConfig