Support fall-back to ASM parsing @Configuration

Update ConfigurationClassParser to fall-back to ASM parsing if standard
annotation processing fails. This change allows @Conditional annotations
that refer to missing classes to work.

This commit also introduces a new inner SourceClass object that
encapsulates the conditional logic required when reading the source
classes.

Issue: SPR-10646
This commit is contained in:
Phillip Webb
2013-06-10 09:18:41 -07:00
parent 51d828816d
commit e10e16cd6b

View File

@@ -17,8 +17,7 @@
package org.springframework.context.annotation;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
@@ -63,7 +62,6 @@ 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.ObjectUtils;
import org.springframework.util.StringUtils;
import static org.springframework.context.annotation.MetadataUtils.*;
@@ -193,38 +191,43 @@ class ConfigurationClassParser {
}
// Recursively process the configuration class and its superclass hierarchy.
AnnotationMetadata metadata = configClass.getMetadata();
SourceClass sourceClass = asSourceClass(configClass);
do {
metadata = doProcessConfigurationClass(configClass, metadata);
sourceClass = doProcessConfigurationClass(configClass, sourceClass);
}
while (metadata != null);
while (sourceClass != null);
this.configurationClasses.add(configClass);
}
/**
* @return annotation metadata of superclass, {@code null} if none found or previously processed
* 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 AnnotationMetadata doProcessConfigurationClass(
ConfigurationClass configClass, AnnotationMetadata metadata) throws IOException {
protected final SourceClass doProcessConfigurationClass(
ConfigurationClass configClass, SourceClass sourceClass) throws IOException {
// recursively process any member (nested) classes first
processMemberClasses(configClass, metadata);
processMemberClasses(configClass, sourceClass);
// process any @PropertySource annotations
AnnotationAttributes propertySource = attributesFor(metadata, org.springframework.context.annotation.PropertySource.class);
AnnotationAttributes propertySource = attributesFor(sourceClass.getMetadata(), org.springframework.context.annotation.PropertySource.class);
if (propertySource != null) {
processPropertySource(propertySource);
}
// process any @ComponentScan annotations
AnnotationAttributes componentScan = attributesFor(metadata, ComponentScan.class);
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<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, metadata.getClassName());
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) {
@@ -236,16 +239,11 @@ class ConfigurationClassParser {
}
// process any @Import annotations
Set<Object> imports = new LinkedHashSet<Object>();
Set<Object> visited = new LinkedHashSet<Object>();
collectImports(metadata, imports, visited);
if (!imports.isEmpty()) {
processImport(configClass, imports, true);
}
processImports(configClass, getImports(sourceClass), true);
// process any @ImportResource annotations
if (metadata.isAnnotated(ImportResource.class.getName())) {
AnnotationAttributes importResource = attributesFor(metadata, ImportResource.class);
if (sourceClass.getMetadata().isAnnotated(ImportResource.class.getName())) {
AnnotationAttributes importResource = attributesFor(sourceClass.getMetadata(), ImportResource.class);
String[] resources = importResource.getStringArray("value");
Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
for (String resource : resources) {
@@ -254,34 +252,22 @@ class ConfigurationClassParser {
}
// process individual @Bean methods
Set<MethodMetadata> beanMethods = metadata.getAnnotatedMethods(Bean.class.getName());
Set<MethodMetadata> beanMethods = sourceClass.getMetadata().getAnnotatedMethods(Bean.class.getName());
for (MethodMetadata methodMetadata : beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}
// process superclass, if any
if (metadata.hasSuperClass()) {
String superclass = metadata.getSuperClassName();
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
if (metadata instanceof StandardAnnotationMetadata) {
Class<?> clazz = ((StandardAnnotationMetadata) metadata).getIntrospectedClass();
return new StandardAnnotationMetadata(clazz.getSuperclass(), true);
try {
return sourceClass.getSuperClass();
}
else if (superclass.startsWith("java")) {
// never load core JDK classes via ASM, in particular not java.lang.Object!
try {
return new StandardAnnotationMetadata(
this.resourceLoader.getClassLoader().loadClass(superclass), true);
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(ex);
}
}
else {
MetadataReader reader = this.metadataReaderFactory.getMetadataReader(superclass);
return reader.getAnnotationMetadata();
catch (ClassNotFoundException ex) {
throw new IllegalStateException(ex);
}
}
}
@@ -292,24 +278,13 @@ class ConfigurationClassParser {
/**
* Register member (nested) classes that happen to be configuration classes themselves.
* @param metadata the metadata representation of the containing class
* @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, AnnotationMetadata metadata) throws IOException {
if (metadata instanceof StandardAnnotationMetadata) {
for (Class<?> memberClass : ((StandardAnnotationMetadata) metadata).getIntrospectedClass().getDeclaredClasses()) {
if (ConfigurationClassUtils.isConfigurationCandidate(new StandardAnnotationMetadata(memberClass))) {
processConfigurationClass(new ConfigurationClass(memberClass, configClass));
}
}
}
else {
for (String memberClassName : metadata.getMemberClassNames()) {
MetadataReader reader = this.metadataReaderFactory.getMetadataReader(memberClassName);
AnnotationMetadata memberClassMetadata = reader.getAnnotationMetadata();
if (ConfigurationClassUtils.isConfigurationCandidate(memberClassMetadata)) {
processConfigurationClass(new ConfigurationClass(reader, configClass));
}
private void processMemberClasses(ConfigurationClass configClass, SourceClass sourceClass) throws IOException {
for (SourceClass memberClass : sourceClass.getMemberClasses()) {
if (ConfigurationClassUtils.isConfigurationCandidate(memberClass.getMetadata())) {
processConfigurationClass(memberClass.asConfigClass(configClass));
}
}
}
@@ -349,59 +324,46 @@ class ConfigurationClassParser {
}
}
/**
* Returns {@code @Import} class, considering all meta-annotations.
*/
private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
Set<SourceClass> imports = new LinkedHashSet<SourceClass>();
Set<SourceClass> visited = new LinkedHashSet<SourceClass>();
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.
* <p>For example, it is common for a {@code @Configuration} class to declare direct
* <p>
* 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 metadata the metadata representation of the class to search
*
* @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(AnnotationMetadata metadata, Set<Object> imports, Set<Object> visited) throws IOException {
String className = metadata.getClassName();
if (visited.add(className)) {
if (metadata instanceof StandardAnnotationMetadata) {
StandardAnnotationMetadata stdMetadata = (StandardAnnotationMetadata) metadata;
for (Annotation ann : stdMetadata.getIntrospectedClass().getAnnotations()) {
if (!ann.annotationType().getName().startsWith("java") && !(ann instanceof Import)) {
collectImports(new StandardAnnotationMetadata(ann.annotationType()), imports, visited);
}
}
Map<String, Object> attributes = stdMetadata.getAnnotationAttributes(Import.class.getName(), false);
if (attributes != null) {
Class[] value = (Class[]) attributes.get("value");
if (!ObjectUtils.isEmpty(value)) {
imports.addAll(Arrays.asList(value));
}
}
}
else {
for (String annotationType : metadata.getAnnotationTypes()) {
if (!className.startsWith("java") && !className.equals(Import.class.getName())) {
try {
collectImports(
new StandardAnnotationMetadata(this.resourceLoader.getClassLoader().loadClass(annotationType)),
imports, visited);
}
catch (ClassNotFoundException ex) {
//
}
}
}
Map<String, Object> attributes = metadata.getAnnotationAttributes(Import.class.getName(), true);
if (attributes != null) {
String[] value = (String[]) attributes.get("value");
if (!ObjectUtils.isEmpty(value)) {
imports.addAll(Arrays.asList(value));
private void collectImports(SourceClass sourceClass, Set<SourceClass> imports,
Set<SourceClass> 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() {
@@ -410,16 +372,21 @@ class ConfigurationClassParser {
try {
ConfigurationClass configClass = deferredImport.getConfigurationClass();
String[] imports = deferredImport.getImportSelector().selectImports(configClass.getMetadata());
processImport(configClass, Arrays.asList(imports), false);
processImports(configClass, asSourceClasses(imports), false);
}
catch (IOException ex) {
catch (Exception ex) {
throw new BeanDefinitionStoreException("Failed to load bean class: ", ex);
}
}
this.deferredImportSelectors.clear();
}
private void processImport(ConfigurationClass configClass, Collection<?> classesToImport, boolean checkForCircularImports) throws IOException {
private void processImports(ConfigurationClass configClass,
Collection<SourceClass> 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()));
}
@@ -427,26 +394,24 @@ class ConfigurationClassParser {
this.importStack.push(configClass);
AnnotationMetadata importingClassMetadata = configClass.getMetadata();
try {
for (Object candidate : classesToImport) {
Object candidateToCheck = (candidate instanceof Class ? (Class) candidate :
this.metadataReaderFactory.getMetadataReader((String) candidate));
if (checkAssignability(ImportSelector.class, candidateToCheck)) {
for (SourceClass candidate : sourceClasses) {
if (candidate.isAssignable(ImportSelector.class)) {
// the candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = (candidate instanceof Class ? (Class) candidate :
this.resourceLoader.getClassLoader().loadClass((String) candidate));
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 {
processImport(configClass, Arrays.asList(selector.selectImports(importingClassMetadata)), false);
String[] importClassNames = selector.selectImports(importingClassMetadata);
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
processImports(configClass, importSourceClasses, false);
}
}
else if (checkAssignability(ImportBeanDefinitionRegistrar.class, candidateToCheck)) {
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// the candidate class is an ImportBeanDefinitionRegistrar -> delegate to it to register additional bean definitions
Class<?> candidateClass = (candidate instanceof Class ? (Class) candidate :
this.resourceLoader.getClassLoader().loadClass((String) candidate));
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar = BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);
invokeAwareMethods(registrar);
configClass.addImportBeanDefinitionRegistrar(registrar);
@@ -454,10 +419,8 @@ class ConfigurationClassParser {
else {
// candidate class not an ImportSelector or ImportBeanDefinitionRegistrar -> process it as a @Configuration class
this.importStack.registerImport(importingClassMetadata.getClassName(),
(candidate instanceof Class ? ((Class) candidate).getName() : (String) candidate));
processConfigurationClass((candidateToCheck instanceof Class ?
new ConfigurationClass((Class) candidateToCheck, configClass) :
new ConfigurationClass((MetadataReader) candidateToCheck, configClass)));
candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass));
}
}
}
@@ -470,15 +433,6 @@ class ConfigurationClassParser {
}
}
private boolean checkAssignability(Class<?> clazz, Object candidate) throws IOException {
if (candidate instanceof Class) {
return clazz.isAssignableFrom((Class) candidate);
}
else {
return new AssignableTypeFilter(clazz).match((MetadataReader) candidate, this.metadataReaderFactory);
}
}
/**
* Invoke {@link ResourceLoaderAware}, {@link BeanClassLoaderAware} and
* {@link BeanFactoryAware} contracts if implemented by the given {@code bean}.
@@ -526,10 +480,68 @@ class ConfigurationClassParser {
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<SourceClass> asSourceClasses(String[] classNamess)
throws ClassNotFoundException, IOException {
List<SourceClass> annotatedClasses = new ArrayList<SourceClass>();
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);
}
@@ -609,6 +621,151 @@ class ConfigurationClassParser {
}
/**
* 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<SourceClass> getMemberClasses() throws IOException {
List<SourceClass> members = new ArrayList<SourceClass>();
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<SourceClass> getAnnotations() throws ClassNotFoundException, IOException {
Set<SourceClass> annotations = new LinkedHashSet<SourceClass>();
for(String annotation : getMetadata().getAnnotationTypes()) {
annotations.add(getRelated(annotation));
}
return annotations;
}
public Collection<SourceClass> getAnnotationAttributes(String annotationType,
String attribute) throws ClassNotFoundException, IOException {
Map<String, Object> annotationAttributes = getMetadata().getAnnotationAttributes(
annotationType, true);
if (annotationAttributes == null
|| !annotationAttributes.containsKey(attribute)) {
return Collections.emptySet();
}
String[] classNames = (String[]) annotationAttributes.get(attribute);
Set<SourceClass> rtn = new LinkedHashSet<SourceClass>();
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}.
*/