From d9f7fdd120409fff4491561215e5b2dda74e2b02 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Wed, 6 Apr 2011 15:41:08 +0800 Subject: [PATCH] Support reading nested annotations via ASM Background Spring 3.1 introduced the @ComponentScan annotation, which can accept an optional array of include and/or exclude @Filter annotations, e.g. @ComponentScan( basePackages = "com.acme.app", includeFilters = { @Filter(MyStereotype.class), ... } ) @Configuration public class AppConfig { ... } @ComponentScan and other annotations related to @Configuration class processing such as @Import, @ImportResource and the @Enable* annotations are parsed using reflection in certain code paths, e.g. when registered directly against AnnotationConfigApplicationContext, and via ASM in other code paths, e.g. when a @Configuration class is discovered via an XML bean definition or when included via the @Import annotation. The ASM-based approach is designed to avoid premature classloading of user types and is instrumental in providing tooling support (STS, etc). Prior to this commit, the ASM-based routines for reading annotation attributes were unable to recurse into nested annotations, such as in the @Filter example above. Prior to Spring 3.1 this was not a problem, because prior to @ComponentScan, there were no cases of nested annotations in the framework. This limitation manifested itself in cases where users encounter the ASM-based annotation parsing code paths AND declare @ComponentScan annotations with explicit nested @Filter annotations. In these cases, the 'includeFilters' and 'excludeFilters' attributes are simply empty where they should be populated, causing the framework to ignore the filter directives and provide incorrect results from component scanning. The purpose of this change then, is to introduce the capability on the ASM side to recurse into nested annotations and annotation arrays. The challenge in doing so is that the nested annotations themselves cannot be realized as annotation instances, so must be represented as a nested Map (or, as described below, the new AnnotationAttributes type). Furthermore, the reflection-based annotation parsing must also be updated to treat nested annotations in a similar fashion; even though the reflection-based approach has no problem accessing nested annotations (it just works out of the box), for substitutability against the AnnotationMetadata SPI, both ASM- and reflection-based implementations should return the same results in any case. Therefore, the reflection-based StandardAnnotationMetadata has also been updated with an optional 'nestedAnnotationsAsMap' constructor argument that is false by default to preserve compatibility in the rare case that StandardAnnotationMetadata is being used outside the core framework. Within the framework, all uses of StandardAnnotationMetadata have been updated to set this new flag to true, meaning that nested annotation results will be consistent regardless the parsing approach used. Spr9031Tests corners this bug and demonstrates that nested @Filter annotations can be parsed and read in both the ASM- and reflection-based paths. Major changes - AnnotationAttributes has been introduced as a concrete LinkedHashMap to be used anywhere annotation attributes are accessed, providing error reporting on attribute lookup and convenient type-safe access to common annotation types such as String, String[], boolean, int, and nested annotation and annotation arrays, with the latter two also returned as AnnotationAttributes instances. - AnnotationUtils#getAnnotationAttributes methods now return AnnotationAttributes instances, even though for binary compatibility the signatures of these methods have been preserved as returning Map. - AnnotationAttributes#forMap provides a convenient mechanism for adapting any Map into an AnnotationAttributes instance. In the case that the Map is already actually of type AnnotationAttributes, it is simply casted and returned. Otherwise, the map is supplied to the AnnotationAttributes(Map) constructor and wrapped in common collections style. - The protected MetadataUtils#attributesFor(Metadata, Class) provides further convenience in the many locations throughout the .context.annotation packagage that depend on annotation attribute introspection. - ASM-based core.type.classreading package reworked Specifically, AnnotationAttributesReadingVisitor has been enhanced to support recursive reading of annotations and annotation arrays, for example in @ComponentScan's nested array of @Filter annotations, ensuring that nested AnnotationAttributes objects are populated as described above. AnnotationAttributesReadingVisitor has also been refactored for clarity, being broken up into several additional ASM AnnotationVisitor implementations. Given that all types are package-private here, these changes represent no risk to binary compatibility. - Reflection-based StandardAnnotationMetadata updated As described above, the 'nestedAnnotationsAsMap' constructor argument has been added, and all framework-internal uses of this class have been updated to set this flag to true. Issue: SPR-7979, SPR-8719, SPR-9031 --- .../AnnotatedGenericBeanDefinition.java | 2 +- .../ComponentScanAnnotationParser.java | 54 ++-- .../annotation/ConfigurationClass.java | 6 +- .../annotation/ConfigurationClassParser.java | 11 +- .../annotation/ConfigurationClassUtils.java | 5 +- .../context/annotation/MetadataUtils.java | 49 ++++ .../configuration/spr9031/Spr9031Tests.java | 79 +++++ .../spr9031/scanpackage/Spr9031Component.java | 24 ++ .../core/annotation/AnnotationAttributes.java | 130 ++++++++ .../core/annotation/AnnotationUtils.java | 65 +++- .../core/type/StandardAnnotationMetadata.java | 41 ++- .../core/type/StandardMethodMetadata.java | 21 +- .../AnnotationAttributesReadingVisitor.java | 277 ++++++++++++------ .../AnnotationMetadataReadingVisitor.java | 44 ++- .../MethodMetadataReadingVisitor.java | 8 +- .../annotation/AnnotationAttributesTests.java | 110 +++++++ .../core/type/AnnotationMetadataTests.java | 188 +++++++++--- 17 files changed, 918 insertions(+), 196 deletions(-) create mode 100644 org.springframework.context/src/main/java/org/springframework/context/annotation/MetadataUtils.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/spr9031/Spr9031Tests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/spr9031/scanpackage/Spr9031Component.java create mode 100644 org.springframework.core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java create mode 100644 org.springframework.core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java 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