diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java index 08e6310753..b913d281b3 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java @@ -23,6 +23,19 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.HashSet; +import org.junit.Test; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.annotation.CustomAutowireConfigurer; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.ComponentScanParserTests.KustomAnnotationAutowiredBean; +import org.springframework.context.annotation.componentscan.simple.SimpleComponent; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.tests.context.SimpleMapScope; +import org.springframework.util.SerializationTestUtils; + import example.scannable.CustomComponent; import example.scannable.CustomStereotype; import example.scannable.DefaultNamedComponent; @@ -33,19 +46,6 @@ import example.scannable_implicitbasepackage.ComponentScanAnnotatedConfigWithImp import example.scannable_scoped.CustomScopeAnnotationBean; import example.scannable_scoped.MyScope; -import org.junit.Test; -import org.springframework.aop.support.AopUtils; -import org.springframework.beans.factory.annotation.CustomAutowireConfigurer; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.context.annotation.ComponentScan.Filter; -import org.springframework.context.annotation.ComponentScanParserTests.CustomAnnotationAutowiredBean; -import org.springframework.context.annotation.componentscan.simple.SimpleComponent; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.tests.context.SimpleMapScope; -import org.springframework.util.SerializationTestUtils; - import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import static org.springframework.beans.factory.support.BeanDefinitionBuilder.*; @@ -159,7 +159,7 @@ public class ComponentScanAnnotationIntegrationTests { @Test public void withCustomTypeFilter() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanWithCustomTypeFilter.class); - CustomAnnotationAutowiredBean testBean = ctx.getBean(CustomAnnotationAutowiredBean.class); + KustomAnnotationAutowiredBean testBean = ctx.getBean(KustomAnnotationAutowiredBean.class); assertThat(testBean.getDependency(), notNullValue()); } @@ -304,8 +304,8 @@ class ComponentScanWithCustomTypeFilter { return cac; } - public ComponentScanParserTests.CustomAnnotationAutowiredBean testBean() { - return new ComponentScanParserTests.CustomAnnotationAutowiredBean(); + public ComponentScanParserTests.KustomAnnotationAutowiredBean testBean() { + return new ComponentScanParserTests.KustomAnnotationAutowiredBean(); } } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserTests.java index 5a4508b903..f9e2435a34 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2014 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. @@ -45,6 +45,7 @@ import example.scannable.AutowiredQualifierFooService; * @author Mark Fisher * @author Juergen Hoeller * @author Chris Beams + * @author Sam Brannen */ public class ComponentScanParserTests { @@ -84,7 +85,7 @@ public class ComponentScanParserTests { public void testCustomAnnotationUsedForBothComponentScanAndQualifier() { ApplicationContext context = new ClassPathXmlApplicationContext( "org/springframework/context/annotation/customAnnotationUsedForBothComponentScanAndQualifierTests.xml"); - CustomAnnotationAutowiredBean testBean = (CustomAnnotationAutowiredBean) context.getBean("testBean"); + KustomAnnotationAutowiredBean testBean = (KustomAnnotationAutowiredBean) context.getBean("testBean"); assertNotNull(testBean.getDependency()); } @@ -92,7 +93,7 @@ public class ComponentScanParserTests { public void testCustomTypeFilter() { ApplicationContext context = new ClassPathXmlApplicationContext( "org/springframework/context/annotation/customTypeFilterTests.xml"); - CustomAnnotationAutowiredBean testBean = (CustomAnnotationAutowiredBean) context.getBean("testBean"); + KustomAnnotationAutowiredBean testBean = (KustomAnnotationAutowiredBean) context.getBean("testBean"); assertNotNull(testBean.getDependency()); } @@ -128,28 +129,36 @@ public class ComponentScanParserTests { } - public static class CustomAnnotationAutowiredBean { + /** + * Intentionally spelling "custom" with a "k" since there are numerous + * classes in this package named *Custom*. + */ + public static class KustomAnnotationAutowiredBean { @Autowired @CustomAnnotation - private CustomAnnotationDependencyBean dependency; + private KustomAnnotationDependencyBean dependency; - public CustomAnnotationDependencyBean getDependency() { + public KustomAnnotationDependencyBean getDependency() { return this.dependency; } } @CustomAnnotation - public static class CustomAnnotationDependencyBean { + public static class KustomAnnotationDependencyBean { } public static class CustomTypeFilter implements TypeFilter { + /** + * Intentionally spelling "custom" with a "k" since there are numerous + * classes in this package named *Custom*. + */ @Override public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) { - return metadataReader.getClassMetadata().getClassName().contains("Custom"); + return metadataReader.getClassMetadata().getClassName().contains("Kustom"); } } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java index b055cf9c15..b808263032 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java @@ -114,73 +114,69 @@ public class ConfigurationClassPostProcessorTests { @Test public void postProcessorWorksWithComposedConfigurationUsingReflection() { RootBeanDefinition beanDefinition = new RootBeanDefinition(ComposedConfigurationClass.class); - postProcessorWorksWithComposedConfiguration(beanDefinition); + assertSupportForComposedAnnotation(beanDefinition); } @Test public void postProcessorWorksWithComposedConfigurationUsingAsm() { RootBeanDefinition beanDefinition = new RootBeanDefinition(ComposedConfigurationClass.class.getName()); - postProcessorWorksWithComposedConfiguration(beanDefinition); + assertSupportForComposedAnnotation(beanDefinition); } @Test public void postProcessorWorksWithComposedConfigurationWithAttributeOverridesUsingReflection() { RootBeanDefinition beanDefinition = new RootBeanDefinition( ComposedConfigurationWithAttributeOverridesClass.class); - postProcessorWorksWithComposedConfigurationWithAttributeOverrides(beanDefinition); + assertSupportForComposedAnnotation(beanDefinition); } - // TODO Remove expected exception when SPR-11574 is resolved. - @Test(expected = ConflictingBeanDefinitionException.class) + @Test public void postProcessorWorksWithComposedConfigurationWithAttributeOverridesUsingAsm() { RootBeanDefinition beanDefinition = new RootBeanDefinition( ComposedConfigurationWithAttributeOverridesClass.class.getName()); - postProcessorWorksWithComposedConfigurationWithAttributeOverrides(beanDefinition); + assertSupportForComposedAnnotation(beanDefinition); } @Test public void postProcessorWorksWithComposedComposedConfigurationWithAttributeOverridesUsingReflection() { RootBeanDefinition beanDefinition = new RootBeanDefinition( ComposedComposedConfigurationWithAttributeOverridesClass.class); - postProcessorWorksWithComposedComposedConfigurationWithAttributeOverrides(beanDefinition); + assertSupportForComposedAnnotation(beanDefinition); } - // TODO Remove expected exception when SPR-11574 is resolved. - @Test(expected = ConflictingBeanDefinitionException.class) + @Test public void postProcessorWorksWithComposedComposedConfigurationWithAttributeOverridesUsingAsm() { RootBeanDefinition beanDefinition = new RootBeanDefinition( ComposedComposedConfigurationWithAttributeOverridesClass.class.getName()); - postProcessorWorksWithComposedComposedConfigurationWithAttributeOverrides(beanDefinition); + assertSupportForComposedAnnotation(beanDefinition); } @Test public void postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverridesUsingReflection() { RootBeanDefinition beanDefinition = new RootBeanDefinition( MetaComponentScanConfigurationWithAttributeOverridesClass.class); - postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverrides(beanDefinition); + assertSupportForComposedAnnotation(beanDefinition); } - // TODO Remove expected exception when SPR-11574 is resolved. - @Test(expected = ConflictingBeanDefinitionException.class) + @Test public void postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverridesUsingAsm() { RootBeanDefinition beanDefinition = new RootBeanDefinition( MetaComponentScanConfigurationWithAttributeOverridesClass.class.getName()); - postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverrides(beanDefinition); + assertSupportForComposedAnnotation(beanDefinition); } @Test public void postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverridesSubclassUsingReflection() { RootBeanDefinition beanDefinition = new RootBeanDefinition( SubMetaComponentScanConfigurationWithAttributeOverridesClass.class); - postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverridesSubclass(beanDefinition); + assertSupportForComposedAnnotation(beanDefinition); } - // TODO Remove expected exception when SPR-11574 is resolved. - @Test(expected = ConflictingBeanDefinitionException.class) + @Test public void postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverridesSubclassUsingAsm() { RootBeanDefinition beanDefinition = new RootBeanDefinition( SubMetaComponentScanConfigurationWithAttributeOverridesClass.class.getName()); - postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverridesSubclass(beanDefinition); + assertSupportForComposedAnnotation(beanDefinition); } @Test @@ -386,46 +382,7 @@ public class ConfigurationClassPostProcessorTests { assertSame(beanFactory.getBean("genericRepo"), beanFactory.getBean("repoConsumer")); } - private void postProcessorWorksWithComposedConfiguration(RootBeanDefinition beanDefinition) { - beanFactory.registerBeanDefinition("config", beanDefinition); - ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); - pp.setEnvironment(new StandardEnvironment()); - pp.postProcessBeanFactory(beanFactory); - SimpleComponent simpleComponent = beanFactory.getBean(SimpleComponent.class); - assertNotNull(simpleComponent); - } - - private void postProcessorWorksWithComposedConfigurationWithAttributeOverrides(RootBeanDefinition beanDefinition) { - beanFactory.registerBeanDefinition("config", beanDefinition); - ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); - pp.setEnvironment(new StandardEnvironment()); - pp.postProcessBeanFactory(beanFactory); - SimpleComponent simpleComponent = beanFactory.getBean(SimpleComponent.class); - assertNotNull(simpleComponent); - } - - private void postProcessorWorksWithComposedComposedConfigurationWithAttributeOverrides( - RootBeanDefinition beanDefinition) { - beanFactory.registerBeanDefinition("config", beanDefinition); - ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); - pp.setEnvironment(new StandardEnvironment()); - pp.postProcessBeanFactory(beanFactory); - SimpleComponent simpleComponent = beanFactory.getBean(SimpleComponent.class); - assertNotNull(simpleComponent); - } - - private void postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverrides( - RootBeanDefinition beanDefinition) { - beanFactory.registerBeanDefinition("config", beanDefinition); - ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); - pp.setEnvironment(new StandardEnvironment()); - pp.postProcessBeanFactory(beanFactory); - SimpleComponent simpleComponent = beanFactory.getBean(SimpleComponent.class); - assertNotNull(simpleComponent); - } - - private void postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverridesSubclass( - RootBeanDefinition beanDefinition) { + private void assertSupportForComposedAnnotation(RootBeanDefinition beanDefinition) { beanFactory.registerBeanDefinition("config", beanDefinition); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.setEnvironment(new StandardEnvironment()); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/customAnnotationUsedForBothComponentScanAndQualifierTests.xml b/spring-context/src/test/java/org/springframework/context/annotation/customAnnotationUsedForBothComponentScanAndQualifierTests.xml index 922aca4bdd..e1b71e7431 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/customAnnotationUsedForBothComponentScanAndQualifierTests.xml +++ b/spring-context/src/test/java/org/springframework/context/annotation/customAnnotationUsedForBothComponentScanAndQualifierTests.xml @@ -17,6 +17,6 @@ - + diff --git a/spring-context/src/test/java/org/springframework/context/annotation/customTypeFilterTests.xml b/spring-context/src/test/java/org/springframework/context/annotation/customTypeFilterTests.xml index f8c1351fe5..d53c33b86e 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/customTypeFilterTests.xml +++ b/spring-context/src/test/java/org/springframework/context/annotation/customTypeFilterTests.xml @@ -17,6 +17,6 @@ - + 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 f09f09709c..735f5fe616 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 @@ -61,7 +61,7 @@ import org.springframework.util.ReflectionUtils; public abstract class AnnotationUtils { /** The attribute name for annotations with a single element */ - static final String VALUE = "value"; + public static final String VALUE = "value"; private static final Map, Boolean> annotatedInterfaceCache = new WeakHashMap, Boolean>(); diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java index c4eb54bf45..4abd7d869b 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -41,6 +41,7 @@ import org.springframework.util.MultiValueMap; * @author Mark Fisher * @author Costin Leau * @author Phillip Webb + * @author Sam Brannen * @since 2.5 */ public class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor implements AnnotationMetadata { @@ -51,7 +52,13 @@ public class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisito protected final Map> metaAnnotationMap = new LinkedHashMap>(4); - protected final MultiValueMap attributeMap = new LinkedMultiValueMap(4); + /** + * Declared as a {@link LinkedMultiValueMap} instead of {@link MultiValueMap} + * in order to ensure that ordering of entries is enforced. + * @see AnnotationReadingVisitorUtils#getMergedAnnotationAttributes(LinkedMultiValueMap, String) + */ + protected final LinkedMultiValueMap attributeMap = new LinkedMultiValueMap( + 4); protected final Set methodMetadataSet = new LinkedHashSet(4); @@ -112,8 +119,8 @@ public class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisito @Override public AnnotationAttributes getAnnotationAttributes(String annotationType, boolean classValuesAsString) { - List attributes = this.attributeMap.get(annotationType); - AnnotationAttributes raw = (attributes == null ? null : attributes.get(0)); + AnnotationAttributes raw = AnnotationReadingVisitorUtils.getMergedAnnotationAttributes(this.attributeMap, + annotationType); return AnnotationReadingVisitorUtils.convertClassValues(this.classLoader, raw, classValuesAsString); } diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationReadingVisitorUtils.java b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationReadingVisitorUtils.java index 50e7395978..88d47e7433 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationReadingVisitorUtils.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationReadingVisitorUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -16,10 +16,17 @@ package org.springframework.core.type.classreading; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.Set; import org.springframework.asm.Type; import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.util.LinkedMultiValueMap; + +import static org.springframework.core.annotation.AnnotationUtils.*; /** * Internal utility class used when reading annotations. @@ -28,12 +35,13 @@ import org.springframework.core.annotation.AnnotationAttributes; * @author Mark Fisher * @author Costin Leau * @author Phillip Webb + * @author Sam Brannen * @since 4.0 */ abstract class AnnotationReadingVisitorUtils { - public static AnnotationAttributes convertClassValues(ClassLoader classLoader, - AnnotationAttributes original, boolean classValuesAsString) { + public static AnnotationAttributes convertClassValues(ClassLoader classLoader, AnnotationAttributes original, + boolean classValuesAsString) { if (original == null) { return null; @@ -44,26 +52,24 @@ abstract class AnnotationReadingVisitorUtils { try { Object value = entry.getValue(); if (value instanceof AnnotationAttributes) { - value = convertClassValues(classLoader, (AnnotationAttributes) value, - classValuesAsString); + value = convertClassValues(classLoader, (AnnotationAttributes) value, classValuesAsString); } else if (value instanceof AnnotationAttributes[]) { - AnnotationAttributes[] values = (AnnotationAttributes[])value; + AnnotationAttributes[] values = (AnnotationAttributes[]) value; for (int i = 0; i < values.length; i++) { - values[i] = convertClassValues(classLoader, values[i], - classValuesAsString); + values[i] = convertClassValues(classLoader, values[i], classValuesAsString); } } else if (value instanceof Type) { - value = (classValuesAsString ? ((Type) value).getClassName() : - classLoader.loadClass(((Type) value).getClassName())); + value = (classValuesAsString ? ((Type) value).getClassName() + : classLoader.loadClass(((Type) value).getClassName())); } else if (value instanceof Type[]) { Type[] array = (Type[]) value; Object[] convArray = (classValuesAsString ? new String[array.length] : new Class[array.length]); for (int i = 0; i < array.length; i++) { - convArray[i] = (classValuesAsString ? array[i].getClassName() : - classLoader.loadClass(array[i].getClassName())); + convArray[i] = (classValuesAsString ? array[i].getClassName() + : classLoader.loadClass(array[i].getClassName())); } value = convArray; } @@ -83,10 +89,66 @@ abstract class AnnotationReadingVisitorUtils { result.put(entry.getKey(), value); } catch (Exception ex) { - // Class not found - can't resolve class reference in annotation attribute. + // Class not found - can't resolve class reference in annotation + // attribute. } } return result; } + /** + * Retrieve the merged attributes of the annotation of the given type, if any, + * from the supplied {@code attributeMap}. + *

Annotation attribute values appearing lower in the annotation + * hierarchy (i.e., closer to the declaring class) will override those + * defined higher in the annotation hierarchy. + * @param attributeMap the map of annotation attribute lists, keyed by + * annotation type + * @param annotationType the annotation type to look for + * @return the merged annotation attributes; or {@code null} if no matching + * annotation is present in the {@code attributeMap} + * @since 4.0.3 + */ + public static AnnotationAttributes getMergedAnnotationAttributes( + LinkedMultiValueMap attributeMap, String annotationType) { + + // Get the unmerged list of attributes for the target annotation. + List attributesList = attributeMap.get(annotationType); + if (attributesList == null || attributesList.isEmpty()) { + return null; + } + + // To start with, we populate the results with all attribute values from the + // target annotation. + AnnotationAttributes results = attributesList.get(0); + Set supportedAttributeNames = results.keySet(); + + // Since the map is a LinkedMultiValueMap, we depend on the ordering of + // elements in the map and reverse the order of the keys in order to traverse + // "down" the meta-annotation hierarchy. + List annotationTypes = new ArrayList(attributeMap.keySet()); + Collections.reverse(annotationTypes); + + for (String currentAnnotationType : annotationTypes) { + if (!currentAnnotationType.startsWith("java.lang.annotation")) { + for (String attributeName : supportedAttributeNames) { + if (!VALUE.equals(attributeName)) { + List currentAttributes = attributeMap.get(currentAnnotationType); + if (currentAttributes != null && !currentAttributes.isEmpty()) { + Object value = currentAttributes.get(0).get(attributeName); + if (value != null) { + // Overwrite value from target annotation with the value + // from an attribute of the same name found lower in the + // meta-annotation hierarchy. + results.put(attributeName, value); + } + } + } + } + } + } + + return results; + } + } diff --git a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java index 484f8207c5..c60594291a 100644 --- a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -16,15 +16,6 @@ package org.springframework.core.type; -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.Matchers.equalTo; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; - -import java.io.IOException; import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; @@ -43,6 +34,9 @@ import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; import org.springframework.stereotype.Component; +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + /** * Unit tests demonstrating that the reflection-based {@link StandardAnnotationMetadata} * and ASM-based {@code AnnotationMetadataReadingVisitor} produce identical output. @@ -50,18 +44,19 @@ import org.springframework.stereotype.Component; * @author Juergen Hoeller * @author Chris Beams * @author Phillip Webb + * @author Sam Brannen */ public class AnnotationMetadataTests { @Test - public void testStandardAnnotationMetadata() throws IOException { + public void standardAnnotationMetadata() throws Exception { AnnotationMetadata metadata = new StandardAnnotationMetadata(AnnotatedComponent.class, true); doTestAnnotationInfo(metadata); doTestMethodAnnotationInfo(metadata); } @Test - public void testAsmAnnotationMetadata() throws IOException { + public void asmAnnotationMetadata() throws Exception { MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(AnnotatedComponent.class.getName()); AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); @@ -77,14 +72,43 @@ public class AnnotationMetadataTests { * 'true' as is done in the main test above. */ @Test - public void testStandardAnnotationMetadata_nestedAnnotationsAsMap_false() throws IOException { + public void standardAnnotationMetadata_nestedAnnotationsAsMap_false() throws Exception { AnnotationMetadata metadata = new StandardAnnotationMetadata(AnnotatedComponent.class); AnnotationAttributes specialAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(SpecialAttr.class.getName()); - Annotation[] nestedAnnoArray = (Annotation[])specialAttrs.get("nestedAnnoArray"); + Annotation[] nestedAnnoArray = (Annotation[]) specialAttrs.get("nestedAnnoArray"); assertThat(nestedAnnoArray[0], instanceOf(NestedAnno.class)); } + @Test + public void metaAnnotationOverridesUsingStandardAnnotationMetadata() { + AnnotationMetadata metadata = new StandardAnnotationMetadata( + ComposedConfigurationWithAttributeOverridesClass.class); + + assertMetaAnnotationOverrides(metadata); + } + + @Test + public void metaAnnotationOverridesUsingAnnotationMetadataReadingVisitor() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(ComposedConfigurationWithAttributeOverridesClass.class.getName()); + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + + assertMetaAnnotationOverrides(metadata); + } + + private void assertMetaAnnotationOverrides(AnnotationMetadata metadata) { + AnnotationAttributes attributes = (AnnotationAttributes) metadata.getAnnotationAttributes( + TestComponentScan.class.getName(), false); + String[] basePackages = attributes.getStringArray("basePackages"); + assertThat("length of basePackages[]", basePackages.length, is(1)); + assertThat("basePackages[0]", basePackages[0], is("org.example.componentscan")); + String[] value = attributes.getStringArray("value"); + assertThat("length of value[]", value.length, is(0)); + Class[] basePackageClasses = attributes.getClassArray("basePackageClasses"); + assertThat("length of basePackageClasses[]", basePackageClasses.length, is(0)); + } + private void doTestAnnotationInfo(AnnotationMetadata metadata) { assertThat(metadata.getClassName(), is(AnnotatedComponent.class.getName())); assertThat(metadata.isInterface(), is(false)); @@ -127,58 +151,59 @@ public class AnnotationMetadataTests { AnnotationAttributes nestedAnno = specialAttrs.getAnnotation("nestedAnno"); assertThat("na", is(nestedAnno.getString("value"))); assertTrue(nestedAnno.getEnum("anEnum").equals(SomeEnum.LABEL1)); - assertArrayEquals(new Class[]{String.class}, (Class[])nestedAnno.get("classArray")); + assertArrayEquals(new Class[] { String.class }, (Class[]) nestedAnno.get("classArray")); AnnotationAttributes[] nestedAnnoArray = specialAttrs.getAnnotationArray("nestedAnnoArray"); assertThat(nestedAnnoArray.length, is(2)); assertThat(nestedAnnoArray[0].getString("value"), is("default")); assertTrue(nestedAnnoArray[0].getEnum("anEnum").equals(SomeEnum.DEFAULT)); - assertArrayEquals(new Class[]{Void.class}, (Class[])nestedAnnoArray[0].get("classArray")); + assertArrayEquals(new Class[] { Void.class }, (Class[]) nestedAnnoArray[0].get("classArray")); assertThat(nestedAnnoArray[1].getString("value"), is("na1")); assertTrue(nestedAnnoArray[1].getEnum("anEnum").equals(SomeEnum.LABEL2)); - assertArrayEquals(new Class[]{Number.class}, (Class[])nestedAnnoArray[1].get("classArray")); - assertArrayEquals(new Class[]{Number.class}, nestedAnnoArray[1].getClassArray("classArray")); + assertArrayEquals(new Class[] { Number.class }, (Class[]) nestedAnnoArray[1].get("classArray")); + assertArrayEquals(new Class[] { Number.class }, nestedAnnoArray[1].getClassArray("classArray")); AnnotationAttributes optional = specialAttrs.getAnnotation("optional"); assertThat(optional.getString("value"), is("optional")); assertTrue(optional.getEnum("anEnum").equals(SomeEnum.DEFAULT)); - assertArrayEquals(new Class[]{Void.class}, (Class[])optional.get("classArray")); - assertArrayEquals(new Class[]{Void.class}, optional.getClassArray("classArray")); + assertArrayEquals(new Class[] { Void.class }, (Class[]) optional.get("classArray")); + assertArrayEquals(new Class[] { Void.class }, optional.getClassArray("classArray")); AnnotationAttributes[] optionalArray = specialAttrs.getAnnotationArray("optionalArray"); assertThat(optionalArray.length, is(1)); assertThat(optionalArray[0].getString("value"), is("optional")); assertTrue(optionalArray[0].getEnum("anEnum").equals(SomeEnum.DEFAULT)); - assertArrayEquals(new Class[]{Void.class}, (Class[])optionalArray[0].get("classArray")); - assertArrayEquals(new Class[]{Void.class}, optionalArray[0].getClassArray("classArray")); + assertArrayEquals(new Class[] { Void.class }, (Class[]) optionalArray[0].get("classArray")); + assertArrayEquals(new Class[] { Void.class }, optionalArray[0].getClassArray("classArray")); assertEquals("direct", metadata.getAnnotationAttributes(DirectAnnotation.class.getName()).get("value")); allMeta = metadata.getAllAnnotationAttributes(DirectAnnotation.class.getName()).get("value"); assertThat(new HashSet(allMeta), is(equalTo(new HashSet(Arrays.asList("direct", "meta"))))); } { // perform tests with classValuesAsString = true - AnnotationAttributes specialAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(SpecialAttr.class.getName(), true); + AnnotationAttributes specialAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes( + SpecialAttr.class.getName(), true); assertThat(specialAttrs.size(), is(6)); - assertThat(specialAttrs.get("clazz"), is((Object)String.class.getName())); + assertThat(specialAttrs.get("clazz"), is((Object) String.class.getName())); assertThat(specialAttrs.getString("clazz"), is(String.class.getName())); AnnotationAttributes nestedAnno = specialAttrs.getAnnotation("nestedAnno"); - assertArrayEquals(new String[]{String.class.getName()}, nestedAnno.getStringArray("classArray")); - assertArrayEquals(new String[]{String.class.getName()}, nestedAnno.getStringArray("classArray")); + assertArrayEquals(new String[] { String.class.getName() }, nestedAnno.getStringArray("classArray")); + assertArrayEquals(new String[] { String.class.getName() }, nestedAnno.getStringArray("classArray")); AnnotationAttributes[] nestedAnnoArray = specialAttrs.getAnnotationArray("nestedAnnoArray"); - assertArrayEquals(new String[]{Void.class.getName()}, (String[])nestedAnnoArray[0].get("classArray")); - assertArrayEquals(new String[]{Void.class.getName()}, nestedAnnoArray[0].getStringArray("classArray")); - assertArrayEquals(new String[]{Number.class.getName()}, (String[])nestedAnnoArray[1].get("classArray")); - assertArrayEquals(new String[]{Number.class.getName()}, nestedAnnoArray[1].getStringArray("classArray")); + assertArrayEquals(new String[] { Void.class.getName() }, (String[]) nestedAnnoArray[0].get("classArray")); + assertArrayEquals(new String[] { Void.class.getName() }, nestedAnnoArray[0].getStringArray("classArray")); + assertArrayEquals(new String[] { Number.class.getName() }, (String[]) nestedAnnoArray[1].get("classArray")); + assertArrayEquals(new String[] { Number.class.getName() }, nestedAnnoArray[1].getStringArray("classArray")); AnnotationAttributes optional = specialAttrs.getAnnotation("optional"); - assertArrayEquals(new String[]{Void.class.getName()}, (String[])optional.get("classArray")); - assertArrayEquals(new String[]{Void.class.getName()}, optional.getStringArray("classArray")); + assertArrayEquals(new String[] { Void.class.getName() }, (String[]) optional.get("classArray")); + assertArrayEquals(new String[] { Void.class.getName() }, optional.getStringArray("classArray")); AnnotationAttributes[] optionalArray = specialAttrs.getAnnotationArray("optionalArray"); - assertArrayEquals(new String[]{Void.class.getName()}, (String[])optionalArray[0].get("classArray")); - assertArrayEquals(new String[]{Void.class.getName()}, optionalArray[0].getStringArray("classArray")); + assertArrayEquals(new String[] { Void.class.getName() }, (String[]) optionalArray[0].get("classArray")); + assertArrayEquals(new String[] { Void.class.getName() }, optionalArray[0].getStringArray("classArray")); assertEquals(metadata.getAnnotationAttributes(DirectAnnotation.class.getName()).get("value"), "direct"); allMeta = metadata.getAllAnnotationAttributes(DirectAnnotation.class.getName()).get("value"); @@ -194,6 +219,9 @@ public class AnnotationMetadataTests { } } + + // ------------------------------------------------------------------------- + public static enum SomeEnum { LABEL1, LABEL2, DEFAULT; } @@ -201,8 +229,11 @@ public class AnnotationMetadataTests { @Target({}) @Retention(RetentionPolicy.RUNTIME) public @interface NestedAnno { + String value() default "default"; + SomeEnum anEnum() default SomeEnum.DEFAULT; + Class[] classArray() default Void.class; } @@ -218,14 +249,15 @@ public class AnnotationMetadataTests { NestedAnno[] nestedAnnoArray(); - NestedAnno optional() default @NestedAnno(value="optional", anEnum=SomeEnum.DEFAULT, classArray=Void.class); + NestedAnno optional() default @NestedAnno(value = "optional", anEnum = SomeEnum.DEFAULT, classArray = Void.class); - NestedAnno[] optionalArray() default {@NestedAnno(value="optional", anEnum=SomeEnum.DEFAULT, classArray=Void.class)}; + NestedAnno[] optionalArray() default { @NestedAnno(value = "optional", anEnum = SomeEnum.DEFAULT, classArray = Void.class) }; } @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) public @interface DirectAnnotation { + String value(); } @@ -250,6 +282,7 @@ public class AnnotationMetadataTests { @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface EnumSubclasses { + SubclassEnum[] value(); } @@ -263,13 +296,9 @@ public class AnnotationMetadataTests { @Component("myName") @Scope("myScope") - @SpecialAttr(clazz = String.class, state = Thread.State.NEW, - nestedAnno = @NestedAnno(value = "na", anEnum = SomeEnum.LABEL1, classArray = {String.class}), - nestedAnnoArray = { - @NestedAnno, - @NestedAnno(value = "na1", anEnum = SomeEnum.LABEL2, classArray = {Number.class}) - }) - @SuppressWarnings({"serial", "unused"}) + @SpecialAttr(clazz = String.class, state = Thread.State.NEW, nestedAnno = @NestedAnno(value = "na", anEnum = SomeEnum.LABEL1, classArray = { String.class }), nestedAnnoArray = { + @NestedAnno, @NestedAnno(value = "na1", anEnum = SomeEnum.LABEL2, classArray = { Number.class }) }) + @SuppressWarnings({ "serial", "unused" }) @DirectAnnotation("direct") @MetaMetaAnnotation @EnumSubclasses({ SubclassEnum.FOO, SubclassEnum.BAR }) @@ -279,7 +308,7 @@ public class AnnotationMetadataTests { public void doWork(@TestQualifier("myColor") java.awt.Color color) { } - public void doSleep() { + public void doSleep() { } @DirectAnnotation("direct") @@ -288,4 +317,36 @@ public class AnnotationMetadataTests { } } + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Component + public static @interface TestConfiguration { + + String value() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public static @interface TestComponentScan { + + String[] value() default {}; + + String[] basePackages() default {}; + + Class[] basePackageClasses() default {}; + } + + @TestConfiguration + @TestComponentScan(basePackages = "bogus") + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public static @interface ComposedConfigurationWithAttributeOverrides { + + String[] basePackages() default {}; + } + + @ComposedConfigurationWithAttributeOverrides(basePackages = "org.example.componentscan") + public static class ComposedConfigurationWithAttributeOverridesClass { + } + }