/* * Copyright 2002-2013 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 java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.Aware; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.ConfigurableBeanFactory; 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.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionReader; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.context.EnvironmentAware; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.NestedIOException; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.Environment; import org.springframework.core.env.PropertySource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.ResourcePropertySource; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.MethodMetadata; import org.springframework.core.type.StandardAnnotationMetadata; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.AssignableTypeFilter; import org.springframework.util.StringUtils; import static org.springframework.context.annotation.MetadataUtils.*; /** * Parses a {@link Configuration} class definition, populating a collection of * {@link ConfigurationClass} objects (parsing a single Configuration class may result in * any number of ConfigurationClass objects because one Configuration class may import * another using the {@link Import} annotation). * *

This class helps separate the concern of parsing the structure of a Configuration * class from the concern of registering BeanDefinition objects based on the * content of that model (with the exception of {@code @ComponentScan} annotations which * need to be registered immediately). * *

This ASM-based implementation avoids reflection and eager class loading in order to * interoperate effectively with lazy class loading in a Spring ApplicationContext. * * @author Chris Beams * @author Juergen Hoeller * @author Phillip Webb * @since 3.0 * @see ConfigurationClassBeanDefinitionReader */ class ConfigurationClassParser { private static final Comparator DEFERRED_IMPORT_COMPARATOR = new Comparator() { @Override public int compare(DeferredImportSelectorHolder o1, DeferredImportSelectorHolder o2) { return AnnotationAwareOrderComparator.INSTANCE.compare(o1.getImportSelector(), o2.getImportSelector()); } }; private final MetadataReaderFactory metadataReaderFactory; private final ProblemReporter problemReporter; private final Environment environment; private final ResourceLoader resourceLoader; private final BeanDefinitionRegistry registry; private final ComponentScanAnnotationParser componentScanParser; private final Set configurationClasses = new LinkedHashSet(); private final Map knownSuperclasses = new HashMap(); private final Stack> propertySources = new Stack>(); private final ImportStack importStack = new ImportStack(); private final List deferredImportSelectors = new LinkedList(); /** * Create a new {@link ConfigurationClassParser} instance that will be used * to populate the set of configuration classes. */ public ConfigurationClassParser(MetadataReaderFactory metadataReaderFactory, ProblemReporter problemReporter, Environment environment, ResourceLoader resourceLoader, BeanNameGenerator componentScanBeanNameGenerator, BeanDefinitionRegistry registry) { this.metadataReaderFactory = metadataReaderFactory; this.problemReporter = problemReporter; this.environment = environment; this.resourceLoader = resourceLoader; this.registry = registry; this.componentScanParser = new ComponentScanAnnotationParser( resourceLoader, environment, componentScanBeanNameGenerator, registry); } public void parse(Set configCandidates) { for (BeanDefinitionHolder holder : configCandidates) { BeanDefinition bd = holder.getBeanDefinition(); try { if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) { parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName()); } else { parse(bd.getBeanClassName(), holder.getBeanName()); } } catch (IOException ex) { throw new BeanDefinitionStoreException("Failed to load bean class: " + bd.getBeanClassName(), ex); } } processDeferredImportSelectors(); } /** * Parse the specified {@link Configuration @Configuration} class. * @param className the name of the class to parse * @param beanName may be null, but if populated represents the bean id * (assumes that this configuration class was configured via XML) */ public void parse(String className, String beanName) throws IOException { MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className); processConfigurationClass(new ConfigurationClass(reader, beanName)); } /** * Parse the specified {@link Configuration @Configuration} class. * @param clazz the Class to parse * @param beanName must not be null (as of Spring 3.1.1) */ public void parse(Class clazz, String beanName) throws IOException { processConfigurationClass(new ConfigurationClass(clazz, beanName)); } protected void processConfigurationClass(ConfigurationClass configClass) throws IOException { if (this.configurationClasses.contains(configClass) && configClass.getBeanName() != null) { // Explicit bean definition found, probably replacing an import. // Let's remove the old one and go with the new one. this.configurationClasses.remove(configClass); for (Iterator it = this.knownSuperclasses.values().iterator(); it.hasNext();) { if (configClass.equals(it.next())) { it.remove(); } } } // Recursively process the configuration class and its superclass hierarchy. SourceClass sourceClass = asSourceClass(configClass); do { sourceClass = doProcessConfigurationClass(configClass, sourceClass); } while (sourceClass != null); this.configurationClasses.add(configClass); } /** * Apply processing and build a complete {@link ConfigurationClass} by reading the * annotations, members and methods from the source class. This method can be called * multiple times as relevant sources are discovered. * @param configClass the configuration class being build * @param sourceClass a source class * @return the superclass, {@code null} if none found or previously processed */ protected final SourceClass doProcessConfigurationClass( ConfigurationClass configClass, SourceClass sourceClass) throws IOException { // recursively process any member (nested) classes first processMemberClasses(configClass, sourceClass); // process any @PropertySource annotations AnnotationAttributes propertySource = attributesFor(sourceClass.getMetadata(), org.springframework.context.annotation.PropertySource.class); if (propertySource != null) { processPropertySource(propertySource); } // process any @ComponentScan annotations AnnotationAttributes componentScan = attributesFor(sourceClass.getMetadata(), ComponentScan.class); if (componentScan != null) { // the config class is annotated with @ComponentScan -> perform the scan immediately if (!ConditionEvaluator.get(configClass.getMetadata(), false).shouldSkip( this.registry, this.environment)) { Set scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); // check the set of scanned definitions for any further config classes and parse recursively if necessary for (BeanDefinitionHolder holder : scannedBeanDefinitions) { if (ConfigurationClassUtils.checkConfigurationClassCandidate(holder.getBeanDefinition(), this.metadataReaderFactory)) { this.parse(holder.getBeanDefinition().getBeanClassName(), holder.getBeanName()); } } } } // process any @Import annotations processImports(configClass, getImports(sourceClass), true); // process any @ImportResource annotations if (sourceClass.getMetadata().isAnnotated(ImportResource.class.getName())) { AnnotationAttributes importResource = attributesFor(sourceClass.getMetadata(), ImportResource.class); String[] resources = importResource.getStringArray("value"); Class readerClass = importResource.getClass("reader"); for (String resource : resources) { configClass.addImportedResource(resource, readerClass); } } // process individual @Bean methods Set beanMethods = sourceClass.getMetadata().getAnnotatedMethods(Bean.class.getName()); for (MethodMetadata methodMetadata : beanMethods) { configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass)); } // process superclass, if any if (sourceClass.getMetadata().hasSuperClass()) { String superclass = sourceClass.getMetadata().getSuperClassName(); if (!this.knownSuperclasses.containsKey(superclass)) { this.knownSuperclasses.put(superclass, configClass); // superclass found, return its annotation metadata and recurse try { return sourceClass.getSuperClass(); } catch (ClassNotFoundException ex) { throw new IllegalStateException(ex); } } } // no superclass, processing is complete return null; } /** * Register member (nested) classes that happen to be configuration classes themselves. * @param sourceClass the source class to process * @throws IOException if there is any problem reading metadata from a member class */ private void processMemberClasses(ConfigurationClass configClass, SourceClass sourceClass) throws IOException { for (SourceClass memberClass : sourceClass.getMemberClasses()) { if (ConfigurationClassUtils.isConfigurationCandidate(memberClass.getMetadata())) { processConfigurationClass(memberClass.asConfigClass(configClass)); } } } /** * Process the given @PropertySource annotation metadata. * @param propertySource metadata for the @PropertySource annotation found * @throws IOException if loading a property source failed */ private void processPropertySource(AnnotationAttributes propertySource) throws IOException { String name = propertySource.getString("name"); String[] locations = propertySource.getStringArray("value"); int nLocations = locations.length; if (nLocations == 0) { throw new IllegalArgumentException("At least one @PropertySource(value) location is required"); } for (int i = 0; i < nLocations; i++) { locations[i] = this.environment.resolveRequiredPlaceholders(locations[i]); } ClassLoader classLoader = this.resourceLoader.getClassLoader(); if (!StringUtils.hasText(name)) { for (String location : locations) { this.propertySources.push(new ResourcePropertySource(location, classLoader)); } } else { if (nLocations == 1) { this.propertySources.push(new ResourcePropertySource(name, locations[0], classLoader)); } else { CompositePropertySource ps = new CompositePropertySource(name); for (String location : locations) { ps.addPropertySource(new ResourcePropertySource(location, classLoader)); } this.propertySources.push(ps); } } } /** * Returns {@code @Import} class, considering all meta-annotations. */ private Set getImports(SourceClass sourceClass) throws IOException { Set imports = new LinkedHashSet(); Set visited = new LinkedHashSet(); collectImports(sourceClass, imports, visited); return imports; } /** * Recursively collect all declared {@code @Import} values. Unlike most * meta-annotations it is valid to have several {@code @Import}s declared with * different values, the usual process or returning values from the first * meta-annotation on a class is not sufficient. *

* For example, it is common for a {@code @Configuration} class to declare direct * {@code @Import}s in addition to meta-imports originating from an {@code @Enable} * annotation. * * @param sourceClass the class to search * @param imports the imports collected so far * @param visited used to track visited classes to prevent infinite recursion * @throws IOException if there is any problem reading metadata from the named class */ private void collectImports(SourceClass sourceClass, Set imports, Set visited) throws IOException { try { if (visited.add(sourceClass)) { for (SourceClass annotation : sourceClass.getAnnotations()) { if(!annotation.getMetadata().getClassName().startsWith("java") && !annotation.isAssignable(Import.class)) { collectImports(annotation, imports, visited); } } imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value")); } } catch (ClassNotFoundException ex) { throw new NestedIOException("Unable to collect imports", ex); } } private void processDeferredImportSelectors() { Collections.sort(this.deferredImportSelectors, DEFERRED_IMPORT_COMPARATOR); for (DeferredImportSelectorHolder deferredImport : this.deferredImportSelectors) { try { ConfigurationClass configClass = deferredImport.getConfigurationClass(); String[] imports = deferredImport.getImportSelector().selectImports(configClass.getMetadata()); processImports(configClass, asSourceClasses(imports), false); } catch (Exception ex) { throw new BeanDefinitionStoreException("Failed to load bean class: ", ex); } } this.deferredImportSelectors.clear(); } private void processImports(ConfigurationClass configClass, Collection sourceClasses, boolean checkForCircularImports) throws IOException { if(sourceClasses.isEmpty()) { return; } if (checkForCircularImports && this.importStack.contains(configClass)) { this.problemReporter.error(new CircularImportProblem(configClass, this.importStack, configClass.getMetadata())); } else { this.importStack.push(configClass); AnnotationMetadata importingClassMetadata = configClass.getMetadata(); try { for (SourceClass candidate : sourceClasses) { if (candidate.isAssignable(ImportSelector.class)) { // the candidate class is an ImportSelector -> delegate to it to determine imports Class candidateClass = candidate.loadClass(); ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class); invokeAwareMethods(selector); if(selector instanceof DeferredImportSelector) { this.deferredImportSelectors.add(new DeferredImportSelectorHolder(configClass, (DeferredImportSelector) selector)); } else { String[] importClassNames = selector.selectImports(importingClassMetadata); Collection importSourceClasses = asSourceClasses(importClassNames); processImports(configClass, importSourceClasses, false); } } else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) { // the candidate class is an ImportBeanDefinitionRegistrar -> delegate to it to register additional bean definitions Class candidateClass = candidate.loadClass(); ImportBeanDefinitionRegistrar registrar = BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class); invokeAwareMethods(registrar); configClass.addImportBeanDefinitionRegistrar(registrar); } else { // candidate class not an ImportSelector or ImportBeanDefinitionRegistrar -> process it as a @Configuration class this.importStack.registerImport(importingClassMetadata.getClassName(), candidate.getMetadata().getClassName()); processConfigurationClass(candidate.asConfigClass(configClass)); } } } catch (ClassNotFoundException ex) { throw new NestedIOException("Failed to load import candidate class", ex); } finally { this.importStack.pop(); } } } /** * Invoke {@link ResourceLoaderAware}, {@link BeanClassLoaderAware} and * {@link BeanFactoryAware} contracts if implemented by the given {@code bean}. */ private void invokeAwareMethods(Object importStrategyBean) { if (importStrategyBean instanceof Aware) { if (importStrategyBean instanceof EnvironmentAware) { ((EnvironmentAware) importStrategyBean).setEnvironment(this.environment); } if (importStrategyBean instanceof ResourceLoaderAware) { ((ResourceLoaderAware) importStrategyBean).setResourceLoader(this.resourceLoader); } if (importStrategyBean instanceof BeanClassLoaderAware) { ClassLoader classLoader = (this.registry instanceof ConfigurableBeanFactory ? ((ConfigurableBeanFactory) this.registry).getBeanClassLoader() : this.resourceLoader.getClassLoader()); ((BeanClassLoaderAware) importStrategyBean).setBeanClassLoader(classLoader); } if (importStrategyBean instanceof BeanFactoryAware && this.registry instanceof BeanFactory) { ((BeanFactoryAware) importStrategyBean).setBeanFactory((BeanFactory) this.registry); } } } /** * Validate each {@link ConfigurationClass} object. * @see ConfigurationClass#validate */ public void validate() { for (ConfigurationClass configClass : this.configurationClasses) { configClass.validate(this.problemReporter); } } public Set getConfigurationClasses() { return this.configurationClasses; } public Stack> getPropertySources() { return this.propertySources; } ImportRegistry getImportRegistry() { return this.importStack; } /** * Factory method to obtain a {@link SourceClass} from a {@link ConfigurationClass}. */ public SourceClass asSourceClass(ConfigurationClass configurationClass) throws IOException { try { AnnotationMetadata metadata = configurationClass.getMetadata(); if (metadata instanceof StandardAnnotationMetadata) { return asSourceClass(((StandardAnnotationMetadata) metadata).getIntrospectedClass()); } return asSourceClass(configurationClass.getMetadata().getClassName()); } catch (ClassNotFoundException ex) { throw new IllegalStateException(ex); } } /** * Factory method to obtain a {@link SourceClass} from a {@link Class}. */ public SourceClass asSourceClass(Class classType) throws ClassNotFoundException, IOException { try { // Sanity test that we can read annotations, if not fall back to ASM classType.getAnnotations(); } catch (Throwable ex) { return asSourceClass(classType.getName()); } return new SourceClass(classType); } /** * Factory method to obtain {@link SourceClass}s from class names. */ public Collection asSourceClasses(String[] classNamess) throws ClassNotFoundException, IOException { List annotatedClasses = new ArrayList(); for (String className : classNamess) { annotatedClasses.add(asSourceClass(className)); } return annotatedClasses; } /** * Factory method to obtain a {@link SourceClass} from a class name. */ public SourceClass asSourceClass(String className) throws ClassNotFoundException, IOException { if (className.startsWith("java")) { // Never use ASM for core java types return new SourceClass(this.resourceLoader.getClassLoader().loadClass( className)); } return new SourceClass(this.metadataReaderFactory.getMetadataReader(className)); } interface ImportRegistry { String getImportingClassFor(String importedClass); } @SuppressWarnings("serial") private static class ImportStack extends Stack implements ImportRegistry { private final Map imports = new HashMap(); public void registerImport(String importingClass, String importedClass) { this.imports.put(importedClass, importingClass); } @Override public String getImportingClassFor(String importedClass) { return this.imports.get(importedClass); } /** * Simplified contains() implementation that tests to see if any {@link ConfigurationClass} * exists within this stack that has the same name as elem. Elem must be of * type ConfigurationClass. */ @Override public boolean contains(Object elem) { ConfigurationClass configClass = (ConfigurationClass) elem; Comparator comparator = new Comparator() { @Override public int compare(ConfigurationClass first, ConfigurationClass second) { return first.getMetadata().getClassName().equals(second.getMetadata().getClassName()) ? 0 : 1; } }; return (Collections.binarySearch(this, configClass, comparator) != -1); } /** * Given a stack containing (in order) *

* return "ImportStack: [Foo->Bar->Baz]". */ @Override public String toString() { StringBuilder builder = new StringBuilder("ImportStack: ["); Iterator iterator = iterator(); while (iterator.hasNext()) { builder.append(iterator.next().getSimpleName()); if (iterator.hasNext()) { builder.append("->"); } } return builder.append(']').toString(); } } private static class DeferredImportSelectorHolder { private final ConfigurationClass configurationClass; private final DeferredImportSelector importSelector; public DeferredImportSelectorHolder(ConfigurationClass configurationClass, DeferredImportSelector importSelector) { this.configurationClass = configurationClass; this.importSelector = importSelector; } public ConfigurationClass getConfigurationClass() { return this.configurationClass; } public DeferredImportSelector getImportSelector() { return this.importSelector; } } /** * Simple wrapper that allows annotated source classes to be dealt with in a uniform * manor, regardless of how they are loaded. */ private class SourceClass { private final Object source; // Class or MetaDataReader private final AnnotationMetadata metadata; private SourceClass(Object source) { this.source = source; if (source instanceof Class) { this.metadata = new StandardAnnotationMetadata((Class) source, true); } else { this.metadata = ((MetadataReader) source).getAnnotationMetadata(); } } public Class loadClass() throws ClassNotFoundException { if(source instanceof Class) { return (Class) source; } String className = ((MetadataReader) source).getClassMetadata().getClassName(); return resourceLoader.getClassLoader().loadClass(className); } public boolean isAssignable(Class clazz) throws IOException { if (source instanceof Class) { return clazz.isAssignableFrom((Class) source); } return new AssignableTypeFilter(clazz).match((MetadataReader) source, metadataReaderFactory); } public ConfigurationClass asConfigClass(ConfigurationClass importedBy) throws IOException { if (this.source instanceof Class) { return new ConfigurationClass((Class) this.source, importedBy); } return new ConfigurationClass((MetadataReader) source, importedBy); } public Collection getMemberClasses() throws IOException { List members = new ArrayList(); if (source instanceof Class) { Class sourceClass = (Class) source; for (Class declaredClass : sourceClass.getDeclaredClasses()) { try { members.add(asSourceClass(declaredClass)); } catch (ClassNotFoundException e) { } } } else { MetadataReader sourceReader = (MetadataReader) source; for (String memberClassName : sourceReader.getClassMetadata().getMemberClassNames()) { try { members.add(asSourceClass(memberClassName)); } catch (ClassNotFoundException e) { } } } return members; } public SourceClass getSuperClass() throws ClassNotFoundException, IOException { if (source instanceof Class) { return asSourceClass(((Class) source).getSuperclass()); } return asSourceClass(((MetadataReader) source).getClassMetadata().getSuperClassName()); } public Set getAnnotations() throws ClassNotFoundException, IOException { Set annotations = new LinkedHashSet(); for(String annotation : getMetadata().getAnnotationTypes()) { annotations.add(getRelated(annotation)); } return annotations; } public Collection getAnnotationAttributes(String annotationType, String attribute) throws ClassNotFoundException, IOException { Map annotationAttributes = getMetadata().getAnnotationAttributes( annotationType, true); if (annotationAttributes == null || !annotationAttributes.containsKey(attribute)) { return Collections.emptySet(); } String[] classNames = (String[]) annotationAttributes.get(attribute); Set rtn = new LinkedHashSet(); for (String className : classNames) { rtn.add(getRelated(className)); } return rtn; } private SourceClass getRelated(String className) throws IOException, ClassNotFoundException { if (source instanceof Class) { try { Class clazz = resourceLoader.getClassLoader().loadClass(className); return asSourceClass(clazz); } catch (ClassNotFoundException ex) { } } return asSourceClass(className); } public AnnotationMetadata getMetadata() { return this.metadata; } @Override public int hashCode() { return toString().hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj == null) { return false; } if (obj instanceof SourceClass) { return toString().equals(obj.toString()); } return false; } @Override public String toString() { return getMetadata().getClassName(); } } /** * {@link Problem} registered upon detection of a circular {@link Import}. */ private static class CircularImportProblem extends Problem { public CircularImportProblem(ConfigurationClass attemptedImport, Stack importStack, AnnotationMetadata metadata) { super(String.format("A circular @Import has been detected: " + "Illegal attempt by @Configuration class '%s' to import class '%s' as '%s' is " + "already present in the current import stack [%s]", importStack.peek().getSimpleName(), attemptedImport.getSimpleName(), attemptedImport.getSimpleName(), importStack), new Location(importStack.peek().getResource(), metadata)); } } }