diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedGenericBeanDefinition.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedGenericBeanDefinition.java index 3f76c252219..5f4ee7f5559 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedGenericBeanDefinition.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedGenericBeanDefinition.java @@ -47,7 +47,7 @@ public class AnnotatedGenericBeanDefinition extends GenericBeanDefinition implem */ public AnnotatedGenericBeanDefinition(Class beanClass) { setBeanClass(beanClass); - this.annotationMetadata = new StandardAnnotationMetadata(beanClass); + this.annotationMetadata = new StandardAnnotationMetadata(beanClass, true); } diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java index 6042babd37a..a9d42ff1564 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 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. @@ -19,20 +19,20 @@ package org.springframework.context.annotation; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Set; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; -import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.core.type.filter.AssignableTypeFilter; import org.springframework.core.type.filter.TypeFilter; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; /** @@ -46,18 +46,23 @@ import org.springframework.util.StringUtils; class ComponentScanAnnotationParser { private final ResourceLoader resourceLoader; + private final Environment environment; + private final BeanDefinitionRegistry registry; - public ComponentScanAnnotationParser(ResourceLoader resourceLoader, Environment environment, BeanDefinitionRegistry registry) { + + public ComponentScanAnnotationParser( + ResourceLoader resourceLoader, Environment environment, BeanDefinitionRegistry registry) { this.resourceLoader = resourceLoader; this.environment = environment; this.registry = registry; } - public Set parse(Map componentScanAttributes) { + + public Set parse(AnnotationAttributes componentScan) { ClassPathBeanDefinitionScanner scanner = - new ClassPathBeanDefinitionScanner(registry, (Boolean)componentScanAttributes.get("useDefaultFilters")); + new ClassPathBeanDefinitionScanner(registry, componentScan.getBoolean("useDefaultFilters")); Assert.notNull(this.environment, "Environment must not be null"); scanner.setEnvironment(this.environment); @@ -66,45 +71,42 @@ class ComponentScanAnnotationParser { scanner.setResourceLoader(this.resourceLoader); scanner.setBeanNameGenerator(BeanUtils.instantiateClass( - (Class)componentScanAttributes.get("nameGenerator"), BeanNameGenerator.class)); + componentScan.getClass("nameGenerator", BeanNameGenerator.class))); - ScopedProxyMode scopedProxyMode = (ScopedProxyMode) componentScanAttributes.get("scopedProxy"); + ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy", ScopedProxyMode.class); if (scopedProxyMode != ScopedProxyMode.DEFAULT) { scanner.setScopedProxyMode(scopedProxyMode); } else { scanner.setScopeMetadataResolver(BeanUtils.instantiateClass( - (Class)componentScanAttributes.get("scopeResolver"), ScopeMetadataResolver.class)); + componentScan.getClass("scopeResolver", ScopeMetadataResolver.class))); } - scanner.setResourcePattern((String)componentScanAttributes.get("resourcePattern")); + scanner.setResourcePattern(componentScan.getString("resourcePattern")); - for (Filter filterAnno : (Filter[])componentScanAttributes.get("includeFilters")) { - for (TypeFilter typeFilter : typeFiltersFor(filterAnno)) { + for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) { + for (TypeFilter typeFilter : typeFiltersFor(filter)) { scanner.addIncludeFilter(typeFilter); } } - for (Filter filterAnno : (Filter[])componentScanAttributes.get("excludeFilters")) { - for (TypeFilter typeFilter : typeFiltersFor(filterAnno)) { + for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) { + for (TypeFilter typeFilter : typeFiltersFor(filter)) { scanner.addExcludeFilter(typeFilter); } } List basePackages = new ArrayList(); - for (String pkg : (String[])componentScanAttributes.get("value")) { + for (String pkg : componentScan.getStringArray("value")) { if (StringUtils.hasText(pkg)) { basePackages.add(pkg); } } - for (String pkg : (String[])componentScanAttributes.get("basePackages")) { + for (String pkg : componentScan.getStringArray("basePackages")) { if (StringUtils.hasText(pkg)) { basePackages.add(pkg); } } - for (Class clazz : (Class[])componentScanAttributes.get("basePackageClasses")) { - // TODO: loading user types directly here. implications on load-time - // weaving may mean we need to revert to stringified class names in - // annotation metadata - basePackages.add(clazz.getPackage().getName()); + for (Class clazz : componentScan.getClassArray("basePackageClasses")) { + basePackages.add(ClassUtils.getPackageName(clazz)); } if (basePackages.isEmpty()) { @@ -114,10 +116,12 @@ class ComponentScanAnnotationParser { return scanner.doScan(basePackages.toArray(new String[]{})); } - private List typeFiltersFor(Filter filterAnno) { + private List typeFiltersFor(AnnotationAttributes filterAttributes) { List typeFilters = new ArrayList(); - for (Class filterClass : (Class[])filterAnno.value()) { - switch (filterAnno.type()) { + FilterType filterType = filterAttributes.getEnum("type", FilterType.class); + + for (Class filterClass : filterAttributes.getClassArray("value")) { + switch (filterType) { case ANNOTATION: Assert.isAssignable(Annotation.class, filterClass, "An error occured when processing a @ComponentScan " + @@ -136,7 +140,7 @@ class ComponentScanAnnotationParser { typeFilters.add(BeanUtils.instantiateClass(filterClass, TypeFilter.class)); break; default: - throw new IllegalArgumentException("unknown filter type " + filterAnno.type()); + throw new IllegalArgumentException("unknown filter type " + filterType); } } return typeFilters; diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java index 7374bc4b342..79467f9fcbd 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2012 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. @@ -97,7 +97,7 @@ final class ConfigurationClass { */ public ConfigurationClass(Class clazz, String beanName) { Assert.hasText(beanName, "bean name must not be null"); - this.metadata = new StandardAnnotationMetadata(clazz); + this.metadata = new StandardAnnotationMetadata(clazz, true); this.resource = new DescriptiveResource(clazz.toString()); this.beanName = beanName; this.imported = false; @@ -112,7 +112,7 @@ final class ConfigurationClass { * @since 3.1.1 */ public ConfigurationClass(Class clazz, boolean imported) { - this.metadata = new StandardAnnotationMetadata(clazz); + this.metadata = new StandardAnnotationMetadata(clazz, true); this.resource = new DescriptiveResource(clazz.toString()); this.imported = imported; } diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index 465826c531c..e3ec57e236b 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 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. @@ -35,6 +35,7 @@ import org.springframework.beans.factory.parsing.Location; import org.springframework.beans.factory.parsing.Problem; import org.springframework.beans.factory.parsing.ProblemReporter; import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.env.Environment; import org.springframework.core.env.PropertySource; import org.springframework.core.io.ResourceLoader; @@ -139,7 +140,7 @@ class ConfigurationClassParser { if (superClassName != null && !Object.class.getName().equals(superClassName)) { if (metadata instanceof StandardAnnotationMetadata) { Class clazz = ((StandardAnnotationMetadata) metadata).getIntrospectedClass(); - metadata = new StandardAnnotationMetadata(clazz.getSuperclass()); + metadata = new StandardAnnotationMetadata(clazz.getSuperclass(), true); } else { MetadataReader reader = this.metadataReaderFactory.getMetadataReader(superClassName); @@ -187,10 +188,10 @@ class ConfigurationClassParser { } // process any @ComponentScan annotions - Map componentScanAttributes = metadata.getAnnotationAttributes(ComponentScan.class.getName()); - if (componentScanAttributes != null) { + AnnotationAttributes componentScan = MetadataUtils.attributesFor(metadata, ComponentScan.class); + if (componentScan != null) { // the config class is annotated with @ComponentScan -> perform the scan immediately - Set scannedBeanDefinitions = this.componentScanParser.parse(componentScanAttributes); + Set scannedBeanDefinitions = this.componentScanParser.parse(componentScan); // check the set of scanned definitions for any further config classes and parse recursively if necessary for (BeanDefinitionHolder holder : scannedBeanDefinitions) { diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java index 5a909771056..645203933af 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 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. @@ -60,7 +60,8 @@ abstract class ConfigurationClassUtils { // Check already loaded Class if present... // since we possibly can't even load the class file for this Class. if (beanDef instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) beanDef).hasBeanClass()) { - metadata = new StandardAnnotationMetadata(((AbstractBeanDefinition) beanDef).getBeanClass()); + Class beanClass = ((AbstractBeanDefinition) beanDef).getBeanClass(); + metadata = new StandardAnnotationMetadata(beanClass, true); } else { String className = beanDef.getBeanClassName(); diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/MetadataUtils.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/MetadataUtils.java new file mode 100644 index 00000000000..a2dc4b36105 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/MetadataUtils.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.MethodMetadata; + +/** + * Convenience methods adapting {@link AnnotationMetadata} and {@link MethodMetadata} + * annotation attribute maps to the {@link AnnotationAttributes} API. As of Spring 3.1.1, + * both the reflection- and ASM-based implementations of these SPIs return + * {@link AnnotationAttributes} instances anyway, but for backward-compatibility, their + * signatures still return Maps. Therefore, for the usual case, these methods perform + * little more than a cast from Map to AnnotationAttributes. + * + * @author Chris Beams + * @since 3.1.1 + * @see AnnotationAttributes#fromMap(java.util.Map) + */ +class MetadataUtils { + + public static AnnotationAttributes attributesFor(AnnotationMetadata metadata, Class annoClass) { + return attributesFor(metadata, annoClass.getName()); + } + + public static AnnotationAttributes attributesFor(AnnotationMetadata metadata, String annoClassName) { + return AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(annoClassName, false)); + } + + public static AnnotationAttributes attributesFor(MethodMetadata metadata, Class targetAnno) { + return AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(targetAnno.getName())); + } + +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/spr9031/Spr9031Tests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/spr9031/Spr9031Tests.java new file mode 100644 index 00000000000..201918a4329 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/spr9031/Spr9031Tests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation.configuration.spr9031; + +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.configuration.spr9031.scanpackage.Spr9031Component; + +/** + * Unit tests cornering bug SPR-9031. + * + * @author Chris Beams + * @since 3.1.1 + */ +public class Spr9031Tests { + + /** + * Use of @Import to register LowLevelConfig results in ASM-based annotation + * processing. + */ + @Test + public void withAsmAnnotationProcessing() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(HighLevelConfig.class); + ctx.refresh(); + assertThat(ctx.getBean(LowLevelConfig.class).scanned, not(nullValue())); + } + + /** + * Direct registration of LowLevelConfig results in reflection-based annotation + * processing. + */ + @Test + public void withoutAsmAnnotationProcessing() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(LowLevelConfig.class); + ctx.refresh(); + assertThat(ctx.getBean(LowLevelConfig.class).scanned, not(nullValue())); + } + + @Configuration + @Import(LowLevelConfig.class) + static class HighLevelConfig {} + + @Configuration + @ComponentScan( + basePackages = "org.springframework.context.annotation.configuration.spr9031.scanpackage", + includeFilters = { @Filter(MarkerAnnotation.class) }) + static class LowLevelConfig { + // fails to wire when LowLevelConfig is processed with ASM because nested @Filter + // annotation is not parsed + @Autowired Spr9031Component scanned; + } + + public @interface MarkerAnnotation {} +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/spr9031/scanpackage/Spr9031Component.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/spr9031/scanpackage/Spr9031Component.java new file mode 100644 index 00000000000..1537b4cbd89 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/spr9031/scanpackage/Spr9031Component.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation.configuration.spr9031.scanpackage; + +import org.springframework.context.annotation.configuration.spr9031.Spr9031Tests.MarkerAnnotation; + +@MarkerAnnotation +public class Spr9031Component { + +} diff --git a/org.springframework.core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java b/org.springframework.core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java new file mode 100644 index 00000000000..110a35bc156 --- /dev/null +++ b/org.springframework.core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.annotation; + +import static java.lang.String.format; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * {@link LinkedHashMap} subclass representing annotation attribute key/value pairs + * as read by Spring's reflection- or ASM-based {@link + * org.springframework.core.type.AnnotationMetadata 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. + * + * @author Chris Beams + * @since 3.1.1 + */ +@SuppressWarnings("serial") +public class AnnotationAttributes extends LinkedHashMap { + + /** + * Create a new, empty {@link AnnotationAttributes} instance. + */ + public AnnotationAttributes() { + } + + /** + * Create a new, empty {@link AnnotationAttributes} instance with the given initial + * capacity to optimize performance. + * @param initialCapacity initial size of the underlying map + */ + public AnnotationAttributes(int initialCapacity) { + super(initialCapacity); + } + + /** + * Create a new {@link AnnotationAttributes} instance, wrapping the provided map + * and all its key/value pairs. + * @param map original source of annotation attribute key/value pairs to wrap + * @see #fromMap(Map) + */ + public AnnotationAttributes(Map map) { + super(map); + } + + /** + * Return an {@link AnnotationAttributes} instance based on the given map; if the map + * is already an {@code AnnotationAttributes} instance, it is casted and returned + * immediately without creating any new instance; otherwise create a new instance by + * wrapping the map with the {@link #AnnotationAttributes(Map)} constructor. + * @param map original source of annotation attribute key/value pairs + */ + public static AnnotationAttributes fromMap(Map map) { + if (map == null) { + return null; + } + + if (map instanceof AnnotationAttributes) { + return (AnnotationAttributes) map; + } + + return new AnnotationAttributes(map); + } + + public String getString(String attributeName) { + return doGet(attributeName, String.class); + } + + public String[] getStringArray(String attributeName) { + return doGet(attributeName, String[].class); + } + + public boolean getBoolean(String attributeName) { + return doGet(attributeName, Boolean.class); + } + + public int getInt(String attributeName) { + return doGet(attributeName, Integer.class); + } + + public > E getEnum(String attributeName, Class enumType) { + return doGet(attributeName, enumType); + } + + @SuppressWarnings("unchecked") + public Class getClass(String attributeName, Class expectedType) { + return (Class)doGet(attributeName, Class.class); + } + + public Class[] getClassArray(String attributeName) { + return doGet(attributeName, Class[].class); + } + + public AnnotationAttributes getAnnotation(String attributeName) { + return doGet(attributeName, AnnotationAttributes.class); + } + + public AnnotationAttributes[] getAnnotationArray(String attributeName) { + return doGet(attributeName, AnnotationAttributes[].class); + } + + @SuppressWarnings("unchecked") + private T doGet(String attributeName, Class expectedType) { + Assert.hasText(attributeName, "attributeName must not be null or empty"); + Object value = this.get(attributeName); + Assert.notNull(value, format("Attribute '%s' not found", attributeName)); + Assert.isAssignable(expectedType, value.getClass(), + format("Attribute '%s' is of type [%s], but [%s] was expected. Cause: ", + attributeName, value.getClass().getSimpleName(), expectedType.getSimpleName())); + return (T) value; + } +} \ No newline at end of file diff --git a/org.springframework.core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java b/org.springframework.core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java index fa08d2b2f54..c8ddfe1fc02 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java +++ b/org.springframework.core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 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. @@ -20,7 +20,6 @@ import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import java.util.Arrays; -import java.util.HashMap; import java.util.Map; import java.util.WeakHashMap; @@ -300,25 +299,58 @@ public abstract class AnnotationUtils { } /** - * Retrieve the given annotation's attributes as a Map, preserving all attribute types as-is. + * Retrieve the given annotation's attributes as a Map, preserving all attribute types + * as-is. + *

Note: As of Spring 3.1.1, the returned map is actually an + * {@link AnnotationAttributes} instance, however the Map signature of this method has + * been preserved for binary compatibility. * @param annotation the annotation to retrieve the attributes for * @return the Map of annotation attributes, with attribute names as keys and * corresponding attribute values as values */ public static Map getAnnotationAttributes(Annotation annotation) { - return getAnnotationAttributes(annotation, false); + return getAnnotationAttributes(annotation, false, false); } /** - * Retrieve the given annotation's attributes as a Map. + * Retrieve the given annotation's attributes as a Map. Equivalent to calling + * {@link #getAnnotationAttributes(Annotation, boolean, boolean)} with + * the {@code nestedAnnotationsAsMap} parameter set to {@code false}. + *

Note: As of Spring 3.1.1, the returned map is actually an + * {@link AnnotationAttributes} instance, however the Map signature of this method has + * been preserved for binary compatibility. * @param annotation the annotation to retrieve the attributes for - * @param classValuesAsString whether to turn Class references into Strings (for compatibility with - * {@link org.springframework.core.type.AnnotationMetadata} or to preserve them as Class references + * @param classValuesAsString whether to turn Class references into Strings (for + * compatibility with {@link org.springframework.core.type.AnnotationMetadata} or to + * preserve them as Class references * @return the Map of annotation attributes, with attribute names as keys and * corresponding attribute values as values */ public static Map getAnnotationAttributes(Annotation annotation, boolean classValuesAsString) { - Map attrs = new HashMap(); + return getAnnotationAttributes(annotation, classValuesAsString, false); + } + + /** + * Retrieve the given annotation's attributes as an {@link AnnotationAttributes} + * map structure. Implemented in Spring 3.1.1 to provide fully recursive annotation + * reading capabilities on par with that of the reflection-based + * {@link org.springframework.core.type.StandardAnnotationMetadata}. + * @param annotation the annotation to retrieve the attributes for + * @param classValuesAsString whether to turn Class references into Strings (for + * compatibility with {@link org.springframework.core.type.AnnotationMetadata} or to + * preserve them as Class references + * @param nestedAnnotationsAsMap whether to turn nested Annotation instances into + * {@link AnnotationAttributes} maps (for compatibility with + * {@link org.springframework.core.type.AnnotationMetadata} or to preserve them as + * Annotation instances + * @return the annotation attributes (a specialized Map) with attribute names as keys + * and corresponding attribute values as values + * @since 3.1.1 + */ + public static AnnotationAttributes getAnnotationAttributes( + Annotation annotation, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { + + AnnotationAttributes attrs = new AnnotationAttributes(); Method[] methods = annotation.annotationType().getDeclaredMethods(); for (Method method : methods) { if (method.getParameterTypes().length == 0 && method.getReturnType() != void.class) { @@ -337,7 +369,22 @@ public abstract class AnnotationUtils { value = newValue; } } - attrs.put(method.getName(), value); + if (nestedAnnotationsAsMap && value instanceof Annotation) { + attrs.put(method.getName(), getAnnotationAttributes( + (Annotation)value, classValuesAsString, nestedAnnotationsAsMap)); + } + else if (nestedAnnotationsAsMap && value instanceof Annotation[]) { + Annotation[] realAnnotations = (Annotation[])value; + AnnotationAttributes[] mappedAnnotations = new AnnotationAttributes[realAnnotations.length]; + for (int i = 0; i < realAnnotations.length; i++) { + mappedAnnotations[i] = getAnnotationAttributes( + realAnnotations[i], classValuesAsString, nestedAnnotationsAsMap); + } + attrs.put(method.getName(), mappedAnnotations); + } + else { + attrs.put(method.getName(), value); + } } catch (Exception ex) { throw new IllegalStateException("Could not obtain annotation attribute values", ex); diff --git a/org.springframework.core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java b/org.springframework.core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java index 5e413033bdf..1e5d00a9ca9 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java +++ b/org.springframework.core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2012 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. @@ -22,24 +22,45 @@ import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; /** * {@link AnnotationMetadata} implementation that uses standard reflection - * to introspect a given Class. + * to introspect a given {@link Class}. * * @author Juergen Hoeller * @author Mark Fisher + * @author Chris Beams * @since 2.5 */ public class StandardAnnotationMetadata extends StandardClassMetadata implements AnnotationMetadata { + private final boolean nestedAnnotationsAsMap; + + /** - * Create a new StandardAnnotationMetadata wrapper for the given Class. + * Create a new {@code StandardAnnotationMetadata} wrapper for the given Class. * @param introspectedClass the Class to introspect + * @see #StandardAnnotationMetadata(Class, boolean) */ - public StandardAnnotationMetadata(Class introspectedClass) { + public StandardAnnotationMetadata(Class introspectedClass) { + this(introspectedClass, false); + } + + /** + * Create a new {@link StandardAnnotationMetadata} wrapper for the given Class, + * providing the option to return any nested annotations or annotation arrays in the + * form of {@link AnnotationAttributes} instead of actual {@link Annotation} instances. + * @param introspectedClass the Class to instrospect + * @param nestedAnnotationsAsMap return nested annotations and annotation arrays as + * {@link AnnotationAttributes} for compatibility with ASM-based + * {@link AnnotationMetadata} implementations + * @since 3.1.1 + */ + public StandardAnnotationMetadata(Class introspectedClass, boolean nestedAnnotationsAsMap) { super(introspectedClass); + this.nestedAnnotationsAsMap = nestedAnnotationsAsMap; } @@ -114,18 +135,20 @@ public class StandardAnnotationMetadata extends StandardClassMetadata implements } public Map getAnnotationAttributes(String annotationType) { - return getAnnotationAttributes(annotationType, false); + return this.getAnnotationAttributes(annotationType, false); } public Map getAnnotationAttributes(String annotationType, boolean classValuesAsString) { Annotation[] anns = getIntrospectedClass().getAnnotations(); for (Annotation ann : anns) { if (ann.annotationType().getName().equals(annotationType)) { - return AnnotationUtils.getAnnotationAttributes(ann, classValuesAsString); + return AnnotationUtils.getAnnotationAttributes( + ann, classValuesAsString, this.nestedAnnotationsAsMap); } for (Annotation metaAnn : ann.annotationType().getAnnotations()) { if (metaAnn.annotationType().getName().equals(annotationType)) { - return AnnotationUtils.getAnnotationAttributes(metaAnn, classValuesAsString); + return AnnotationUtils.getAnnotationAttributes( + metaAnn, classValuesAsString, this.nestedAnnotationsAsMap); } } } @@ -157,13 +180,13 @@ public class StandardAnnotationMetadata extends StandardClassMetadata implements for (Method method : methods) { for (Annotation ann : method.getAnnotations()) { if (ann.annotationType().getName().equals(annotationType)) { - annotatedMethods.add(new StandardMethodMetadata(method)); + annotatedMethods.add(new StandardMethodMetadata(method, this.nestedAnnotationsAsMap)); break; } else { for (Annotation metaAnn : ann.annotationType().getAnnotations()) { if (metaAnn.annotationType().getName().equals(annotationType)) { - annotatedMethods.add(new StandardMethodMetadata(method)); + annotatedMethods.add(new StandardMethodMetadata(method, this.nestedAnnotationsAsMap)); break; } } diff --git a/org.springframework.core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java b/org.springframework.core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java index ead840cc21b..aca42b9d946 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java +++ b/org.springframework.core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2012 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. @@ -37,14 +37,27 @@ public class StandardMethodMetadata implements MethodMetadata { private final Method introspectedMethod; + private final boolean nestedAnnotationsAsMap; + /** * Create a new StandardMethodMetadata wrapper for the given Method. * @param introspectedMethod the Method to introspect */ public StandardMethodMetadata(Method introspectedMethod) { + this(introspectedMethod, false); + } + + /** + * Create a new StandardMethodMetadata wrapper for the given Method. + * @param introspectedMethod the Method to introspect + * @param nestedAnnotationsAsMap + * @since 3.1.1 + */ + public StandardMethodMetadata(Method introspectedMethod, boolean nestedAnnotationsAsMap) { Assert.notNull(introspectedMethod, "Method must not be null"); this.introspectedMethod = introspectedMethod; + this.nestedAnnotationsAsMap = nestedAnnotationsAsMap; } /** @@ -94,11 +107,13 @@ public class StandardMethodMetadata implements MethodMetadata { Annotation[] anns = this.introspectedMethod.getAnnotations(); for (Annotation ann : anns) { if (ann.annotationType().getName().equals(annotationType)) { - return AnnotationUtils.getAnnotationAttributes(ann, true); + return AnnotationUtils.getAnnotationAttributes( + ann, true, nestedAnnotationsAsMap); } for (Annotation metaAnn : ann.annotationType().getAnnotations()) { if (metaAnn.annotationType().getName().equals(annotationType)) { - return AnnotationUtils.getAnnotationAttributes(metaAnn, true); + return AnnotationUtils.getAnnotationAttributes( + metaAnn, true, this.nestedAnnotationsAsMap); } } } diff --git a/org.springframework.core/src/main/java/org/springframework/core/type/classreading/AnnotationAttributesReadingVisitor.java b/org.springframework.core/src/main/java/org/springframework/core/type/classreading/AnnotationAttributesReadingVisitor.java index 244808b3235..7f48ece4fd5 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/type/classreading/AnnotationAttributesReadingVisitor.java +++ b/org.springframework.core/src/main/java/org/springframework/core/type/classreading/AnnotationAttributesReadingVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2012 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. @@ -20,131 +20,242 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.util.LinkedHashMap; +import java.util.ArrayList; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.asm.AnnotationVisitor; import org.springframework.asm.Type; -import org.springframework.asm.commons.EmptyVisitor; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; + /** - * ASM visitor which looks for the annotations defined on a class or method. - * + * @author Chris Beams * @author Juergen Hoeller - * @since 3.0 + * @since 3.1.1 */ -final class AnnotationAttributesReadingVisitor implements AnnotationVisitor { +abstract class AbstractRecursiveAnnotationVisitor implements AnnotationVisitor { - private final String annotationType; + protected final Log logger = LogFactory.getLog(this.getClass()); - private final Map> attributesMap; + protected final AnnotationAttributes attributes; - private final Map> metaAnnotationMap; - - private final ClassLoader classLoader; - - private final Map localAttributes = new LinkedHashMap(); + protected final ClassLoader classLoader; - public AnnotationAttributesReadingVisitor( - String annotationType, Map> attributesMap, - Map> metaAnnotationMap, ClassLoader classLoader) { - - this.annotationType = annotationType; - this.attributesMap = attributesMap; - this.metaAnnotationMap = metaAnnotationMap; + public AbstractRecursiveAnnotationVisitor(ClassLoader classLoader, AnnotationAttributes attributes) { this.classLoader = classLoader; + this.attributes = attributes; } - public void visit(String name, Object value) { - this.localAttributes.put(name, value); + public void visit(String attributeName, Object attributeValue) { + this.attributes.put(attributeName, attributeValue); } - public void visitEnum(String name, String desc, String value) { - Object valueToUse = value; + public AnnotationVisitor visitAnnotation(String attributeName, String asmTypeDescriptor) { + String annotationType = Type.getType(asmTypeDescriptor).getClassName(); + AnnotationAttributes nestedAttributes = new AnnotationAttributes(); + this.attributes.put(attributeName, nestedAttributes); + return new RecursiveAnnotationAttributesVisitor(annotationType, nestedAttributes, this.classLoader); + } + + public AnnotationVisitor visitArray(String attributeName) { + return new RecursiveAnnotationArrayVisitor(attributeName, this.attributes, this.classLoader); + } + + public void visitEnum(String attributeName, String asmTypeDescriptor, String attributeValue) { + Object valueToUse = attributeValue; try { - Class enumType = this.classLoader.loadClass(Type.getType(desc).getClassName()); - Field enumConstant = ReflectionUtils.findField(enumType, value); + Class enumType = this.classLoader.loadClass(Type.getType(asmTypeDescriptor).getClassName()); + Field enumConstant = ReflectionUtils.findField(enumType, attributeValue); if (enumConstant != null) { valueToUse = enumConstant.get(null); } } catch (Exception ex) { - // Class not found - can't resolve class reference in annotation attribute. + logNonFatalException(ex); } - this.localAttributes.put(name, valueToUse); + this.attributes.put(attributeName, valueToUse); } - public AnnotationVisitor visitAnnotation(String name, String desc) { - return new EmptyVisitor(); + + protected void logNonFatalException(Exception ex) { + this.logger.warn("Failed to classload type while reading annotation metadata. " + + "This is a non-fatal error, but certain annotation metadata may be " + + "unavailable.", ex); + } +} + + +/** + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1.1 + */ +final class RecursiveAnnotationArrayVisitor extends AbstractRecursiveAnnotationVisitor { + + private final String attributeName; + + private final List allNestedAttributes = new ArrayList(); + + + public RecursiveAnnotationArrayVisitor( + String attributeName, AnnotationAttributes attributes, ClassLoader classLoader) { + super(classLoader, attributes); + this.attributeName = attributeName; } - public AnnotationVisitor visitArray(final String attrName) { - return new AnnotationVisitor() { - public void visit(String name, Object value) { - Object newValue = value; - Object existingValue = localAttributes.get(attrName); - if (existingValue != null) { - newValue = ObjectUtils.addObjectToArray((Object[]) existingValue, newValue); - } - else { - Object[] newArray = (Object[]) Array.newInstance(newValue.getClass(), 1); - newArray[0] = newValue; - newValue = newArray; - } - localAttributes.put(attrName, newValue); - } - public void visitEnum(String name, String desc, String value) { - } - public AnnotationVisitor visitAnnotation(String name, String desc) { - return new EmptyVisitor(); - } - public AnnotationVisitor visitArray(String name) { - return new EmptyVisitor(); - } - public void visitEnd() { - } - }; + @Override + public void visit(String attributeName, Object attributeValue) { + Object newValue = attributeValue; + Object existingValue = this.attributes.get(this.attributeName); + if (existingValue != null) { + newValue = ObjectUtils.addObjectToArray((Object[]) existingValue, newValue); + } + else { + Object[] newArray = (Object[]) Array.newInstance(newValue.getClass(), 1); + newArray[0] = newValue; + newValue = newArray; + } + this.attributes.put(this.attributeName, newValue); + } + + @Override + public AnnotationVisitor visitAnnotation(String attributeName, String asmTypeDescriptor) { + String annotationType = Type.getType(asmTypeDescriptor).getClassName(); + AnnotationAttributes nestedAttributes = new AnnotationAttributes(); + this.allNestedAttributes.add(nestedAttributes); + return new RecursiveAnnotationAttributesVisitor(annotationType, nestedAttributes, this.classLoader); } public void visitEnd() { - this.attributesMap.put(this.annotationType, this.localAttributes); + if (!this.allNestedAttributes.isEmpty()) { + this.attributes.put(this.attributeName, this.allNestedAttributes.toArray( + new AnnotationAttributes[this.allNestedAttributes.size()])); + } + } +} + + +/** + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1.1 + */ +class RecursiveAnnotationAttributesVisitor extends AbstractRecursiveAnnotationVisitor { + + private final String annotationType; + + + public RecursiveAnnotationAttributesVisitor( + String annotationType, AnnotationAttributes attributes, ClassLoader classLoader) { + super(classLoader, attributes); + this.annotationType = annotationType; + } + + + public final void visitEnd() { try { Class annotationClass = this.classLoader.loadClass(this.annotationType); - // Check declared default values of attributes in the annotation type. - Method[] annotationAttributes = annotationClass.getMethods(); - for (Method annotationAttribute : annotationAttributes) { - String attributeName = annotationAttribute.getName(); - Object defaultValue = annotationAttribute.getDefaultValue(); - if (defaultValue != null && !this.localAttributes.containsKey(attributeName)) { - this.localAttributes.put(attributeName, defaultValue); - } - } - // Register annotations that the annotation type is annotated with. - Set metaAnnotationTypeNames = new LinkedHashSet(); - for (Annotation metaAnnotation : annotationClass.getAnnotations()) { - metaAnnotationTypeNames.add(metaAnnotation.annotationType().getName()); - if (!this.attributesMap.containsKey(metaAnnotation.annotationType().getName())) { - this.attributesMap.put(metaAnnotation.annotationType().getName(), - AnnotationUtils.getAnnotationAttributes(metaAnnotation, true)); - } - for (Annotation metaMetaAnnotation : metaAnnotation.annotationType().getAnnotations()) { - metaAnnotationTypeNames.add(metaMetaAnnotation.annotationType().getName()); - } - } - if (this.metaAnnotationMap != null) { - this.metaAnnotationMap.put(this.annotationType, metaAnnotationTypeNames); - } + this.doVisitEnd(annotationClass); } catch (ClassNotFoundException ex) { - // Class not found - can't determine meta-annotations. + logNonFatalException(ex); } } + protected void doVisitEnd(Class annotationClass) { + registerDefaultValues(annotationClass); + } + + private void registerDefaultValues(Class annotationClass) { + // Check declared default values of attributes in the annotation type. + Method[] annotationAttributes = annotationClass.getMethods(); + for (Method annotationAttribute : annotationAttributes) { + String attributeName = annotationAttribute.getName(); + Object defaultValue = annotationAttribute.getDefaultValue(); + if (defaultValue != null && !this.attributes.containsKey(attributeName)) { + if (defaultValue instanceof Annotation) { + defaultValue = AnnotationAttributes.fromMap( + AnnotationUtils.getAnnotationAttributes((Annotation)defaultValue, false, true)); + } + else if (defaultValue instanceof Annotation[]) { + Annotation[] realAnnotations = (Annotation[]) defaultValue; + AnnotationAttributes[] mappedAnnotations = new AnnotationAttributes[realAnnotations.length]; + for (int i = 0; i < realAnnotations.length; i++) { + mappedAnnotations[i] = AnnotationAttributes.fromMap( + AnnotationUtils.getAnnotationAttributes(realAnnotations[i], false, true)); + } + defaultValue = mappedAnnotations; + } + this.attributes.put(attributeName, defaultValue); + } + } + } } + + +/** + * ASM visitor which looks for the annotations defined on a class or method, including + * tracking meta-annotations. + * + *

As of Spring 3.1.1, this visitor is fully recursive, taking into account any nested + * annotations or nested annotation arrays. These annotations are in turn read into + * {@link AnnotationAttributes} map structures. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 3.0 + */ +final class AnnotationAttributesReadingVisitor extends RecursiveAnnotationAttributesVisitor { + + private final String annotationType; + + private final Map attributesMap; + + private final Map> metaAnnotationMap; + + + public AnnotationAttributesReadingVisitor( + String annotationType, Map attributesMap, + Map> metaAnnotationMap, ClassLoader classLoader) { + + super(annotationType, new AnnotationAttributes(), classLoader); + this.annotationType = annotationType; + this.attributesMap = attributesMap; + this.metaAnnotationMap = metaAnnotationMap; + } + + @Override + public void doVisitEnd(Class annotationClass) { + super.doVisitEnd(annotationClass); + this.attributesMap.put(this.annotationType, this.attributes); + registerMetaAnnotations(annotationClass); + } + + private void registerMetaAnnotations(Class annotationClass) { + // Register annotations that the annotation type is annotated with. + Set metaAnnotationTypeNames = new LinkedHashSet(); + for (Annotation metaAnnotation : annotationClass.getAnnotations()) { + metaAnnotationTypeNames.add(metaAnnotation.annotationType().getName()); + if (!this.attributesMap.containsKey(metaAnnotation.annotationType().getName())) { + this.attributesMap.put(metaAnnotation.annotationType().getName(), + AnnotationUtils.getAnnotationAttributes(metaAnnotation, true, true)); + } + for (Annotation metaMetaAnnotation : metaAnnotation.annotationType().getAnnotations()) { + metaAnnotationTypeNames.add(metaMetaAnnotation.annotationType().getName()); + } + } + if (this.metaAnnotationMap != null) { + this.metaAnnotationMap.put(annotationClass.getName(), metaAnnotationTypeNames); + } + } +} \ No newline at end of file diff --git a/org.springframework.core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java b/org.springframework.core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java index cf76c807c91..97a4897f815 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java +++ b/org.springframework.core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 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. @@ -26,6 +26,7 @@ import java.util.Set; import org.springframework.asm.AnnotationVisitor; import org.springframework.asm.MethodVisitor; import org.springframework.asm.Type; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.MethodMetadata; import org.springframework.util.CollectionUtils; @@ -50,7 +51,7 @@ final class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor private final Map> metaAnnotationMap = new LinkedHashMap>(4); - private final Map> attributeMap = new LinkedHashMap>(4); + private final Map attributeMap = new LinkedHashMap(4); private final MultiValueMap methodMetadataMap = new LinkedMultiValueMap(); @@ -99,20 +100,41 @@ final class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor return this.attributeMap.containsKey(annotationType); } - public Map getAnnotationAttributes(String annotationType) { + public AnnotationAttributes getAnnotationAttributes(String annotationType) { return getAnnotationAttributes(annotationType, false); } - public Map getAnnotationAttributes(String annotationType, boolean classValuesAsString) { - Map raw = this.attributeMap.get(annotationType); - if (raw == null) { + public AnnotationAttributes getAnnotationAttributes(String annotationType, boolean classValuesAsString) { + return getAnnotationAttributes(annotationType, classValuesAsString, false); + } + + public AnnotationAttributes getAnnotationAttributes( + String annotationType, boolean classValuesAsString, boolean nestedAttributesAsMap) { + + AnnotationAttributes raw = this.attributeMap.get(annotationType); + return convertClassValues(raw, classValuesAsString, nestedAttributesAsMap); + } + + private AnnotationAttributes convertClassValues( + AnnotationAttributes original, boolean classValuesAsString, boolean nestedAttributesAsMap) { + + if (original == null) { return null; } - Map result = new LinkedHashMap(raw.size()); - for (Map.Entry entry : raw.entrySet()) { + AnnotationAttributes result = new AnnotationAttributes(original.size()); + for (Map.Entry entry : original.entrySet()) { try { Object value = entry.getValue(); - if (value instanceof Type) { + if (value instanceof AnnotationAttributes) { + value = convertClassValues((AnnotationAttributes)value, classValuesAsString, nestedAttributesAsMap); + } + else if (value instanceof AnnotationAttributes[]) { + AnnotationAttributes[] values = (AnnotationAttributes[])value; + for (int i = 0; i < values.length; i++) { + values[i] = convertClassValues(values[i], classValuesAsString, nestedAttributesAsMap); + } + } + else if (value instanceof Type) { value = (classValuesAsString ? ((Type) value).getClassName() : this.classLoader.loadClass(((Type) value).getClassName())); } @@ -127,10 +149,10 @@ final class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor } else if (classValuesAsString) { if (value instanceof Class) { - value = ((Class) value).getName(); + value = ((Class) value).getName(); } else if (value instanceof Class[]) { - Class[] clazzArray = (Class[]) value; + Class[] clazzArray = (Class[]) value; String[] newValue = new String[clazzArray.length]; for (int i = 0; i < clazzArray.length; i++) { newValue[i] = clazzArray[i].getName(); diff --git a/org.springframework.core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java b/org.springframework.core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java index 1bb40cbbee8..43476425aad 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java +++ b/org.springframework.core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2012 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. @@ -24,6 +24,7 @@ import org.springframework.asm.MethodAdapter; import org.springframework.asm.Opcodes; import org.springframework.asm.Type; import org.springframework.asm.commons.EmptyVisitor; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.MethodMetadata; import org.springframework.util.MultiValueMap; @@ -35,6 +36,7 @@ import org.springframework.util.MultiValueMap; * @author Juergen Hoeller * @author Mark Pollack * @author Costin Leau + * @author Chris Beams * @since 3.0 */ final class MethodMetadataReadingVisitor extends MethodAdapter implements MethodMetadata { @@ -49,7 +51,7 @@ final class MethodMetadataReadingVisitor extends MethodAdapter implements Method private final MultiValueMap methodMetadataMap; - private final Map> attributeMap = new LinkedHashMap>(2); + private final Map attributeMap = new LinkedHashMap(2); public MethodMetadataReadingVisitor(String name, int access, String declaringClassName, ClassLoader classLoader, MultiValueMap methodMetadataMap) { @@ -88,7 +90,7 @@ final class MethodMetadataReadingVisitor extends MethodAdapter implements Method return this.attributeMap.containsKey(annotationType); } - public Map getAnnotationAttributes(String annotationType) { + public AnnotationAttributes getAnnotationAttributes(String annotationType) { return this.attributeMap.get(annotationType); } diff --git a/org.springframework.core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java b/org.springframework.core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java new file mode 100644 index 00000000000..75869ae79e8 --- /dev/null +++ b/org.springframework.core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.annotation; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Test; + +/** + * Unit tests for {@link AnnotationAttributes}. + * + * @author Chris Beams + * @since 3.1.1 + */ +public class AnnotationAttributesTests { + + enum Color { RED, WHITE, BLUE } + + @Test + public void testTypeSafeAttributeAccess() { + AnnotationAttributes a = new AnnotationAttributes(); + a.put("name", "dave"); + a.put("names", new String[] { "dave", "frank", "hal" }); + a.put("bool1", true); + a.put("bool2", false); + a.put("color", Color.RED); + a.put("clazz", Integer.class); + a.put("classes", new Class[] { Number.class, Short.class, Integer.class }); + a.put("number", 42); + a.put("numbers", new int[] { 42, 43 }); + AnnotationAttributes anno = new AnnotationAttributes(); + anno.put("value", 10); + anno.put("name", "algernon"); + a.put("anno", anno); + a.put("annoArray", new AnnotationAttributes[] { anno }); + + assertThat(a.getString("name"), equalTo("dave")); + assertThat(a.getStringArray("names"), equalTo(new String[] { "dave", "frank", "hal" })); + assertThat(a.getBoolean("bool1"), equalTo(true)); + assertThat(a.getBoolean("bool2"), equalTo(false)); + assertThat(a.getEnum("color", Color.class), equalTo(Color.RED)); + assertTrue(a.getClass("clazz", Number.class).equals(Integer.class)); + assertThat(a.getClassArray("classes"), equalTo(new Class[] { Number.class, Short.class, Integer.class })); + assertThat(a.getInt("number"), equalTo(42)); + assertThat(a.getAnnotation("anno").getInt("value"), equalTo(10)); + assertThat(a.getAnnotationArray("annoArray")[0].getString("name"), equalTo("algernon")); + } + + @Test + public void getEnum_emptyAttributeName() { + AnnotationAttributes a = new AnnotationAttributes(); + a.put("color", "RED"); + try { + a.getEnum("", Color.class); + fail(); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage(), equalTo("attributeName must not be null or empty")); + } + try { + a.getEnum(null, Color.class); + fail(); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage(), equalTo("attributeName must not be null or empty")); + } + } + + @Test + public void getEnum_notFound() { + AnnotationAttributes a = new AnnotationAttributes(); + a.put("color", "RED"); + try { + a.getEnum("colour", Color.class); + fail(); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage(), equalTo("Attribute 'colour' not found")); + } + } + + @Test + public void getEnum_typeMismatch() { + AnnotationAttributes a = new AnnotationAttributes(); + a.put("color", "RED"); + try { + a.getEnum("color", Color.class); + fail(); + } catch (IllegalArgumentException ex) { + String expected = + "Attribute 'color' is of type [String], but [Color] was expected"; + assertThat(ex.getMessage().substring(0, expected.length()), equalTo(expected)); + } + } + +} diff --git a/org.springframework.core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java b/org.springframework.core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java index 952b565be05..8dcc5f74669 100644 --- a/org.springframework.core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java +++ b/org.springframework.core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2012 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,99 +16,203 @@ package org.springframework.core.type; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertArrayEquals; +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; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.Map; import java.util.Set; -import junit.framework.TestCase; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; import org.springframework.stereotype.Component; /** + * Unit tests demonstrating that the reflection-based {@link StandardAnnotationMetadata} + * and ASM-based {@code AnnotationMetadataReadingVisitor} produce identical output. + * * @author Juergen Hoeller + * @author Chris Beams */ -public class AnnotationMetadataTests extends TestCase { +public class AnnotationMetadataTests { + @Test public void testStandardAnnotationMetadata() throws IOException { - StandardAnnotationMetadata annInfo = new StandardAnnotationMetadata(AnnotatedComponent.class); - doTestAnnotationInfo(annInfo); - doTestMethodAnnotationInfo(annInfo); + AnnotationMetadata metadata = new StandardAnnotationMetadata(AnnotatedComponent.class, true); + doTestAnnotationInfo(metadata); + doTestMethodAnnotationInfo(metadata); } + @Test public void testAsmAnnotationMetadata() throws IOException { MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(AnnotatedComponent.class.getName()); - doTestAnnotationInfo(metadataReader.getAnnotationMetadata()); - doTestMethodAnnotationInfo(metadataReader.getAnnotationMetadata()); + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + doTestAnnotationInfo(metadata); + doTestMethodAnnotationInfo(metadata); + } + + /** + * In order to preserve backward-compatibility, {@link StandardAnnotationMetadata} + * defaults to return nested annotations and annotation arrays as actual + * Annotation instances. It is recommended for compatibility with ASM-based + * AnnotationMetadata implementations to set the 'nestedAnnotationsAsMap' flag to + * 'true' as is done in the main test above. + */ + @Test + public void testStandardAnnotationMetadata_nestedAnnotationsAsMap_false() throws IOException { + AnnotationMetadata metadata = new StandardAnnotationMetadata(AnnotatedComponent.class); + + AnnotationAttributes specialAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(SpecialAttr.class.getName()); + Annotation[] nestedAnnoArray = (Annotation[])specialAttrs.get("nestedAnnoArray"); + assertThat(nestedAnnoArray[0], instanceOf(NestedAnno.class)); } private void doTestAnnotationInfo(AnnotationMetadata metadata) { - assertEquals(AnnotatedComponent.class.getName(), metadata.getClassName()); - assertFalse(metadata.isInterface()); - assertFalse(metadata.isAbstract()); - assertTrue(metadata.isConcrete()); - assertTrue(metadata.hasSuperClass()); - assertEquals(Object.class.getName(), metadata.getSuperClassName()); - assertEquals(1, metadata.getInterfaceNames().length); - assertEquals(Serializable.class.getName(), metadata.getInterfaceNames()[0]); + assertThat(metadata.getClassName(), is(AnnotatedComponent.class.getName())); + assertThat(metadata.isInterface(), is(false)); + assertThat(metadata.isAbstract(), is(false)); + assertThat(metadata.isConcrete(), is(true)); + assertThat(metadata.hasSuperClass(), is(true)); + assertThat(metadata.getSuperClassName(), is(Object.class.getName())); + assertThat(metadata.getInterfaceNames().length, is(1)); + assertThat(metadata.getInterfaceNames()[0], is(Serializable.class.getName())); - assertTrue(metadata.hasAnnotation(Component.class.getName())); - assertTrue(metadata.hasAnnotation(Scope.class.getName())); - assertTrue(metadata.hasAnnotation(SpecialAttr.class.getName())); - assertEquals(3, metadata.getAnnotationTypes().size()); - assertTrue(metadata.getAnnotationTypes().contains(Component.class.getName())); - assertTrue(metadata.getAnnotationTypes().contains(Scope.class.getName())); - assertTrue(metadata.getAnnotationTypes().contains(SpecialAttr.class.getName())); + assertThat(metadata.hasAnnotation(Component.class.getName()), is(true)); + assertThat(metadata.hasAnnotation(Scope.class.getName()), is(true)); + assertThat(metadata.hasAnnotation(SpecialAttr.class.getName()), is(true)); + assertThat(metadata.getAnnotationTypes().size(), is(3)); + assertThat(metadata.getAnnotationTypes().contains(Component.class.getName()), is(true)); + assertThat(metadata.getAnnotationTypes().contains(Scope.class.getName()), is(true)); + assertThat(metadata.getAnnotationTypes().contains(SpecialAttr.class.getName()), is(true)); - Map compAttrs = metadata.getAnnotationAttributes(Component.class.getName()); - assertEquals(1, compAttrs.size()); - assertEquals("myName", compAttrs.get("value")); - Map scopeAttrs = metadata.getAnnotationAttributes(Scope.class.getName()); - assertEquals(1, scopeAttrs.size()); - assertEquals("myScope", scopeAttrs.get("value")); - Map specialAttrs = metadata.getAnnotationAttributes(SpecialAttr.class.getName()); - assertEquals(2, specialAttrs.size()); - assertEquals(String.class, specialAttrs.get("clazz")); - assertEquals(Thread.State.NEW, specialAttrs.get("state")); - Map specialAttrsString = metadata.getAnnotationAttributes(SpecialAttr.class.getName(), true); - assertEquals(String.class.getName(), specialAttrsString .get("clazz")); - assertEquals(Thread.State.NEW, specialAttrsString.get("state")); + AnnotationAttributes compAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(Component.class.getName()); + assertThat(compAttrs.size(), is(1)); + assertThat(compAttrs.getString("value"), is("myName")); + AnnotationAttributes scopeAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(Scope.class.getName()); + assertThat(scopeAttrs.size(), is(1)); + assertThat(scopeAttrs.getString("value"), is("myScope")); + { // perform tests with classValuesAsString = false (the default) + AnnotationAttributes specialAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(SpecialAttr.class.getName()); + assertThat(specialAttrs.size(), is(6)); + assertTrue(String.class.isAssignableFrom(specialAttrs.getClass("clazz", Object.class))); + assertThat(specialAttrs.getEnum("state", Thread.State.class), is(Thread.State.NEW)); + + AnnotationAttributes nestedAnno = specialAttrs.getAnnotation("nestedAnno"); + assertThat("na", is(nestedAnno.getString("value"))); + assertThat(nestedAnno.getEnum("anEnum", SomeEnum.class), is(SomeEnum.LABEL1)); + 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")); + assertThat(nestedAnnoArray[0].getEnum("anEnum", SomeEnum.class), is(SomeEnum.DEFAULT)); + assertArrayEquals(new Class[]{Void.class}, (Class[])nestedAnnoArray[0].get("classArray")); + assertThat(nestedAnnoArray[1].getString("value"), is("na1")); + assertThat(nestedAnnoArray[1].getEnum("anEnum", SomeEnum.class), is(SomeEnum.LABEL2)); + 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")); + assertThat(optional.getEnum("anEnum", SomeEnum.class), is(SomeEnum.DEFAULT)); + 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")); + assertThat(optionalArray[0].getEnum("anEnum", SomeEnum.class), is(SomeEnum.DEFAULT)); + assertArrayEquals(new Class[]{Void.class}, (Class[])optionalArray[0].get("classArray")); + assertArrayEquals(new Class[]{Void.class}, optionalArray[0].getClassArray("classArray")); + } + { // perform tests with classValuesAsString = 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.getString("clazz"), is(String.class.getName())); + + AnnotationAttributes nestedAnno = specialAttrs.getAnnotation("nestedAnno"); + assertArrayEquals(new String[]{String.class.getName()}, (String[])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")); + + AnnotationAttributes optional = specialAttrs.getAnnotation("optional"); + 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")); + } } private void doTestMethodAnnotationInfo(AnnotationMetadata classMetadata) { Set methods = classMetadata.getAnnotatedMethods(Autowired.class.getName()); - assertEquals(1, methods.size()); + assertThat(methods.size(), is(1)); for (MethodMetadata methodMetadata : methods) { - assertTrue(methodMetadata.isAnnotated(Autowired.class.getName())); + assertThat(methodMetadata.isAnnotated(Autowired.class.getName()), is(true)); } - } + public static enum SomeEnum { + LABEL1, LABEL2, DEFAULT; + } + + @Target({}) + @Retention(RetentionPolicy.RUNTIME) + public @interface NestedAnno { + String value() default "default"; + SomeEnum anEnum() default SomeEnum.DEFAULT; + Class[] classArray() default Void.class; + } @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface SpecialAttr { - Class clazz(); + Class clazz(); Thread.State state(); + + NestedAnno nestedAnno(); + + NestedAnno[] nestedAnnoArray(); + + NestedAnno optional() default @NestedAnno(value="optional", anEnum=SomeEnum.DEFAULT, classArray=Void.class); + + NestedAnno[] optionalArray() default {@NestedAnno(value="optional", anEnum=SomeEnum.DEFAULT, classArray=Void.class)}; } @Component("myName") @Scope("myScope") - @SpecialAttr(clazz = String.class, state = Thread.State.NEW) + @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"}) private static class AnnotatedComponent implements Serializable { @Autowired