Skip to content

Commit d0c31ad

Browse files
committed
Allow recursive use of @componentscan
Prior to this change, @componentscan annotations were only processed at the first level of depth. Now, the set of bean definitions resulting from each declaration of @componentscan is checked for configuration classes that declare @componentscan, and recursion is performed as necessary. Cycles between @componentscan declarations are detected as well. See CircularComponentScanException. Issue: SPR-8307
1 parent c2b030a commit d0c31ad

15 files changed

+429
-108
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2002-2011 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.context.annotation;
18+
19+
/**
20+
* Exception thrown upon detection of circular {@link ComponentScan} use.
21+
*
22+
* @author Chris Beams
23+
* @since 3.1
24+
*/
25+
@SuppressWarnings("serial")
26+
class CircularComponentScanException extends IllegalStateException {
27+
28+
public CircularComponentScanException(String message, Exception cause) {
29+
super(message, cause);
30+
}
31+
32+
}

org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ protected void registerBeanDefinition(BeanDefinitionHolder definitionHolder, Bea
269269
* @return <code>true</code> if the bean can be registered as-is;
270270
* <code>false</code> if it should be skipped because there is an
271271
* existing, compatible bean definition for the specified name
272-
* @throws IllegalStateException if an existing, incompatible
272+
* @throws ConflictingBeanDefinitionException if an existing, incompatible
273273
* bean definition has been found for the specified name
274274
*/
275275
protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException {
@@ -284,7 +284,7 @@ protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition)
284284
if (isCompatible(beanDefinition, existingDef)) {
285285
return false;
286286
}
287-
throw new IllegalStateException("Annotation-specified bean name '" + beanName +
287+
throw new ConflictingBeanDefinitionException("Annotation-specified bean name '" + beanName +
288288
"' for bean class [" + beanDefinition.getBeanClassName() + "] conflicts with existing, " +
289289
"non-compatible bean definition of same name and class [" + existingDef.getBeanClassName() + "]");
290290
}

org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@
2020
import java.util.ArrayList;
2121
import java.util.List;
2222
import java.util.Map;
23+
import java.util.Set;
2324

2425
import org.springframework.beans.BeanUtils;
26+
import org.springframework.beans.factory.config.BeanDefinitionHolder;
2527
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
2628
import org.springframework.beans.factory.support.BeanNameGenerator;
2729
import org.springframework.context.annotation.ComponentScan.Filter;
2830
import org.springframework.core.env.Environment;
2931
import org.springframework.core.io.ResourceLoader;
30-
import org.springframework.core.type.AnnotationMetadata;
3132
import org.springframework.core.type.filter.AnnotationTypeFilter;
3233
import org.springframework.core.type.filter.AssignableTypeFilter;
3334
import org.springframework.core.type.filter.TypeFilter;
@@ -54,15 +55,9 @@ public ComponentScanAnnotationParser(ResourceLoader resourceLoader, Environment
5455
this.registry = registry;
5556
}
5657

57-
public void parse(AnnotationMetadata annotationMetadata) {
58-
Map<String, Object> attribs = annotationMetadata.getAnnotationAttributes(ComponentScan.class.getName());
59-
if (attribs == null) {
60-
// @ComponentScan annotation is not present -> do nothing
61-
return;
62-
}
63-
58+
public Set<BeanDefinitionHolder> parse(Map<String, Object> componentScanAttributes) {
6459
ClassPathBeanDefinitionScanner scanner =
65-
new ClassPathBeanDefinitionScanner(registry, (Boolean)attribs.get("useDefaultFilters"));
60+
new ClassPathBeanDefinitionScanner(registry, (Boolean)componentScanAttributes.get("useDefaultFilters"));
6661

6762
Assert.notNull(this.environment, "Environment must not be null");
6863
scanner.setEnvironment(this.environment);
@@ -71,37 +66,37 @@ public void parse(AnnotationMetadata annotationMetadata) {
7166
scanner.setResourceLoader(this.resourceLoader);
7267

7368
scanner.setBeanNameGenerator(BeanUtils.instantiateClass(
74-
(Class<?>)attribs.get("nameGenerator"), BeanNameGenerator.class));
69+
(Class<?>)componentScanAttributes.get("nameGenerator"), BeanNameGenerator.class));
7570

76-
ScopedProxyMode scopedProxyMode = (ScopedProxyMode) attribs.get("scopedProxy");
71+
ScopedProxyMode scopedProxyMode = (ScopedProxyMode) componentScanAttributes.get("scopedProxy");
7772
if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
7873
scanner.setScopedProxyMode(scopedProxyMode);
7974
} else {
8075
scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(
81-
(Class<?>)attribs.get("scopeResolver"), ScopeMetadataResolver.class));
76+
(Class<?>)componentScanAttributes.get("scopeResolver"), ScopeMetadataResolver.class));
8277
}
8378

84-
scanner.setResourcePattern((String)attribs.get("resourcePattern"));
79+
scanner.setResourcePattern((String)componentScanAttributes.get("resourcePattern"));
8580

86-
for (Filter filter : (Filter[])attribs.get("includeFilters")) {
81+
for (Filter filter : (Filter[])componentScanAttributes.get("includeFilters")) {
8782
scanner.addIncludeFilter(createTypeFilter(filter));
8883
}
89-
for (Filter filter : (Filter[])attribs.get("excludeFilters")) {
84+
for (Filter filter : (Filter[])componentScanAttributes.get("excludeFilters")) {
9085
scanner.addExcludeFilter(createTypeFilter(filter));
9186
}
9287

9388
List<String> basePackages = new ArrayList<String>();
94-
for (String pkg : (String[])attribs.get("value")) {
89+
for (String pkg : (String[])componentScanAttributes.get("value")) {
9590
if (StringUtils.hasText(pkg)) {
9691
basePackages.add(pkg);
9792
}
9893
}
99-
for (String pkg : (String[])attribs.get("basePackages")) {
94+
for (String pkg : (String[])componentScanAttributes.get("basePackages")) {
10095
if (StringUtils.hasText(pkg)) {
10196
basePackages.add(pkg);
10297
}
10398
}
104-
for (Class<?> clazz : (Class<?>[])attribs.get("basePackageClasses")) {
99+
for (Class<?> clazz : (Class<?>[])componentScanAttributes.get("basePackageClasses")) {
105100
// TODO: loading user types directly here. implications on load-time
106101
// weaving may mean we need to revert to stringified class names in
107102
// annotation metadata
@@ -112,7 +107,7 @@ public void parse(AnnotationMetadata annotationMetadata) {
112107
throw new IllegalStateException("At least one base package must be specified");
113108
}
114109

115-
scanner.scan(basePackages.toArray(new String[]{}));
110+
return scanner.doScan(basePackages.toArray(new String[]{}));
116111
}
117112

118113
private TypeFilter createTypeFilter(Filter filter) {

org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java

Lines changed: 4 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727

2828
import org.apache.commons.logging.Log;
2929
import org.apache.commons.logging.LogFactory;
30-
import org.springframework.beans.BeanUtils;
3130
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
3231
import org.springframework.beans.factory.annotation.Autowire;
3332
import org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor;
@@ -44,19 +43,12 @@
4443
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
4544
import org.springframework.beans.factory.support.GenericBeanDefinition;
4645
import org.springframework.beans.factory.support.RootBeanDefinition;
47-
import org.springframework.context.EnvironmentAware;
48-
import org.springframework.context.ResourceLoaderAware;
49-
import org.springframework.core.Conventions;
50-
import org.springframework.core.env.Environment;
5146
import org.springframework.core.io.Resource;
5247
import org.springframework.core.io.ResourceLoader;
5348
import org.springframework.core.type.AnnotationMetadata;
5449
import org.springframework.core.type.MethodMetadata;
55-
import org.springframework.core.type.StandardAnnotationMetadata;
5650
import org.springframework.core.type.classreading.MetadataReader;
5751
import org.springframework.core.type.classreading.MetadataReaderFactory;
58-
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
59-
import org.springframework.stereotype.Component;
6052
import org.springframework.util.StringUtils;
6153

6254
/**
@@ -74,15 +66,8 @@
7466
*/
7567
public class ConfigurationClassBeanDefinitionReader {
7668

77-
private static final String CONFIGURATION_CLASS_FULL = "full";
78-
79-
private static final String CONFIGURATION_CLASS_LITE = "lite";
80-
81-
private static final String CONFIGURATION_CLASS_ATTRIBUTE =
82-
Conventions.getQualifiedAttributeName(ConfigurationClassPostProcessor.class, "configurationClass");
83-
8469
private static final Log logger = LogFactory.getLog(ConfigurationClassBeanDefinitionReader.class);
85-
70+
8671
private final BeanDefinitionRegistry registry;
8772

8873
private final SourceExtractor sourceExtractor;
@@ -93,28 +78,21 @@ public class ConfigurationClassBeanDefinitionReader {
9378

9479
private ResourceLoader resourceLoader;
9580

96-
private Environment environment;
97-
98-
private final ComponentScanAnnotationParser componentScanParser;
99-
10081
/**
10182
* Create a new {@link ConfigurationClassBeanDefinitionReader} instance that will be used
10283
* to populate the given {@link BeanDefinitionRegistry}.
10384
* @param problemReporter
10485
* @param metadataReaderFactory
10586
*/
106-
public ConfigurationClassBeanDefinitionReader(final BeanDefinitionRegistry registry, SourceExtractor sourceExtractor,
87+
public ConfigurationClassBeanDefinitionReader(BeanDefinitionRegistry registry, SourceExtractor sourceExtractor,
10788
ProblemReporter problemReporter, MetadataReaderFactory metadataReaderFactory,
108-
ResourceLoader resourceLoader, Environment environment) {
89+
ResourceLoader resourceLoader) {
10990

11091
this.registry = registry;
11192
this.sourceExtractor = sourceExtractor;
11293
this.problemReporter = problemReporter;
11394
this.metadataReaderFactory = metadataReaderFactory;
11495
this.resourceLoader = resourceLoader;
115-
this.environment = environment;
116-
117-
this.componentScanParser = new ComponentScanAnnotationParser(resourceLoader, environment, registry);
11896
}
11997

12098

@@ -133,8 +111,6 @@ public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
133111
* class itself, all its {@link Bean} methods
134112
*/
135113
private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass) {
136-
AnnotationMetadata metadata = configClass.getMetadata();
137-
componentScanParser.parse(metadata);
138114
doLoadBeanDefinitionForConfigurationClassIfNecessary(configClass);
139115
for (BeanMethod beanMethod : configClass.getBeanMethods()) {
140116
loadBeanDefinitionsForBeanMethod(beanMethod);
@@ -155,7 +131,7 @@ private void doLoadBeanDefinitionForConfigurationClassIfNecessary(ConfigurationC
155131
BeanDefinition configBeanDef = new GenericBeanDefinition();
156132
String className = configClass.getMetadata().getClassName();
157133
configBeanDef.setBeanClassName(className);
158-
if (checkConfigurationClassCandidate(configBeanDef, this.metadataReaderFactory)) {
134+
if (ConfigurationClassUtils.checkConfigurationClassCandidate(configBeanDef, this.metadataReaderFactory)) {
159135
String configBeanName = BeanDefinitionReaderUtils.registerWithGeneratedName((AbstractBeanDefinition)configBeanDef, this.registry);
160136
configClass.setBeanName(configBeanName);
161137
if (logger.isDebugEnabled()) {
@@ -311,59 +287,6 @@ private void loadBeanDefinitionsFromImportedResources(Map<String, Class<?>> impo
311287
}
312288

313289

314-
/**
315-
* Check whether the given bean definition is a candidate for a configuration class,
316-
* and mark it accordingly.
317-
* @param beanDef the bean definition to check
318-
* @param metadataReaderFactory the current factory in use by the caller
319-
* @return whether the candidate qualifies as (any kind of) configuration class
320-
*/
321-
public static boolean checkConfigurationClassCandidate(BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) {
322-
AnnotationMetadata metadata = null;
323-
324-
// Check already loaded Class if present...
325-
// since we possibly can't even load the class file for this Class.
326-
if (beanDef instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) beanDef).hasBeanClass()) {
327-
metadata = new StandardAnnotationMetadata(((AbstractBeanDefinition) beanDef).getBeanClass());
328-
}
329-
else {
330-
String className = beanDef.getBeanClassName();
331-
if (className != null) {
332-
try {
333-
MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(className);
334-
metadata = metadataReader.getAnnotationMetadata();
335-
}
336-
catch (IOException ex) {
337-
if (logger.isDebugEnabled()) {
338-
logger.debug("Could not find class file for introspecting factory methods: " + className, ex);
339-
}
340-
return false;
341-
}
342-
}
343-
}
344-
345-
if (metadata != null) {
346-
if (metadata.isAnnotated(Configuration.class.getName())) {
347-
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
348-
return true;
349-
}
350-
else if (metadata.isAnnotated(Component.class.getName()) ||
351-
metadata.hasAnnotatedMethods(Bean.class.getName())) {
352-
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
353-
return true;
354-
}
355-
}
356-
return false;
357-
}
358-
359-
/**
360-
* Determine whether the given bean definition indicates a full @Configuration class.
361-
*/
362-
public static boolean isFullConfigurationClass(BeanDefinition beanDef) {
363-
return CONFIGURATION_CLASS_FULL.equals(beanDef.getAttribute(CONFIGURATION_CLASS_ATTRIBUTE));
364-
}
365-
366-
367290
/**
368291
* {@link RootBeanDefinition} marker subclass used to signify that a bean definition
369292
* was created from a configuration class as opposed to any other configuration source.

org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,14 @@
2828
import java.util.Stack;
2929

3030
import org.springframework.beans.BeanUtils;
31+
import org.springframework.beans.factory.config.BeanDefinitionHolder;
3132
import org.springframework.beans.factory.parsing.Location;
3233
import org.springframework.beans.factory.parsing.Problem;
3334
import org.springframework.beans.factory.parsing.ProblemReporter;
35+
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
3436
import org.springframework.core.annotation.AnnotationUtils;
3537
import org.springframework.core.env.Environment;
38+
import org.springframework.core.io.ResourceLoader;
3639
import org.springframework.core.type.AnnotationMetadata;
3740
import org.springframework.core.type.MethodMetadata;
3841
import org.springframework.core.type.StandardAnnotationMetadata;
@@ -72,16 +75,24 @@ class ConfigurationClassParser {
7275

7376
private final Environment environment;
7477

78+
private final ResourceLoader resourceLoader;
79+
80+
private final ComponentScanAnnotationParser componentScanParser;
81+
7582

7683
/**
7784
* Create a new {@link ConfigurationClassParser} instance that will be used
7885
* to populate the set of configuration classes.
7986
*/
8087
public ConfigurationClassParser(MetadataReaderFactory metadataReaderFactory,
81-
ProblemReporter problemReporter, Environment environment) {
88+
ProblemReporter problemReporter, Environment environment,
89+
ResourceLoader resourceLoader, BeanDefinitionRegistry registry) {
8290
this.metadataReaderFactory = metadataReaderFactory;
8391
this.problemReporter = problemReporter;
8492
this.environment = environment;
93+
this.resourceLoader = resourceLoader;
94+
95+
this.componentScanParser = new ComponentScanAnnotationParser(this.resourceLoader, this.environment, registry);
8596
}
8697

8798

@@ -141,6 +152,25 @@ protected void processConfigurationClass(ConfigurationClass configClass) throws
141152
}
142153

143154
protected void doProcessConfigurationClass(ConfigurationClass configClass, AnnotationMetadata metadata) throws IOException {
155+
Map<String, Object> componentScanAttributes = metadata.getAnnotationAttributes(ComponentScan.class.getName());
156+
if (componentScanAttributes != null) {
157+
// the config class is annotated with @ComponentScan -> perform the scan immediately
158+
Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScanAttributes);
159+
160+
// check the set of scanned definitions for any further config classes and parse recursively if necessary
161+
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
162+
if (ConfigurationClassUtils.checkConfigurationClassCandidate(holder.getBeanDefinition(), metadataReaderFactory)) {
163+
try {
164+
this.parse(holder.getBeanDefinition().getBeanClassName(), holder.getBeanName());
165+
} catch (ConflictingBeanDefinitionException ex) {
166+
throw new CircularComponentScanException(
167+
"A conflicting bean definition was detected while processing @ComponentScan annotations. " +
168+
"This usually indicates a circle between scanned packages.", ex);
169+
}
170+
}
171+
}
172+
}
173+
144174
List<Map<String, Object>> allImportAttribs =
145175
AnnotationUtils.findAllAnnotationAttributes(Import.class, metadata.getClassName(), true);
146176
for (Map<String, Object> importAttribs : allImportAttribs) {

0 commit comments

Comments
 (0)