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<String, Object> 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<String, Object>.

 - AnnotationAttributes#forMap provides a convenient mechanism for
   adapting any Map<String, Object> 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
This commit is contained in:
Chris Beams
2011-04-06 15:41:08 +08:00
parent 905d17d444
commit d9f7fdd120
17 changed files with 918 additions and 196 deletions

View File

@@ -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<BeanDefinitionHolder> parse(Map<String, Object> componentScanAttributes) {
public Set<BeanDefinitionHolder> 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<String> basePackages = new ArrayList<String>();
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<TypeFilter> typeFiltersFor(Filter filterAnno) {
private List<TypeFilter> typeFiltersFor(AnnotationAttributes filterAttributes) {
List<TypeFilter> typeFilters = new ArrayList<TypeFilter>();
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;

View File

@@ -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;
}

View File

@@ -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<String, Object> 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<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScanAttributes);
Set<BeanDefinitionHolder> 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) {

View File

@@ -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();

View File

@@ -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()));
}
}