Bean Definition Generation
The lifecycle of a Spring bean begins with the creation and registration of BeanDefinition objects. This process is primarily handled by the ClassPathBeanDefinitionScanner, which identifies candidate classes in specified base packages. The scanner relies on a BeanDefinitionRegistry, typically implemented by DefaultListableBeanFactory, to store the resulting definitions.
Scanning and Candidate Identification
The core scanning logic resides in the doScan method. It iterates through base packages, identifies candidate components, applies scope metadata, generates bean names, and registers the definitions.
protected Set<BeanDefinitionHolder> performScan(String... basePackages) {
Set<BeanDefinitionHolder> registeredDefinitions = new LinkedHashSet<>();
for (String pkg : basePackages) {
Set<BeanDefinition> candidates = findPotentialComponents(pkg);
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.nameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
applyDefaults((AbstractBeanDefinition) candidate);
}
if (candidate instanceof AnnotatedBeanDefinition) {
processDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
if (isCandidateValid(beanName, candidate)) {
BeanDefinitionHolder holder = new BeanDefinitionHolder(candidate, beanName);
registeredDefinitions.add(holder);
registerDefinition(holder, this.registry);
}
}
}
return registeredDefinitions;
}
The findPotentialComponents method leverages ASM (Abstract Syntax Tree) technology to read class metadata without actually loading the classes. This optimization ensures resource efficiency during startup. The method constructs a search path, retrieves resources, and applies filters to determine eligibility.
private Set<BeanDefinition> locateCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
String searchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
Resource[] resources = getResourcePatternResolver().getResources(searchPath);
for (Resource resource : resources) {
if (resource.isReadable()) {
MetadataReader reader = getMetadataReaderFactory().getMetadataReader(resource);
if (matchesFilterConditions(reader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(reader);
sbd.setSource(resource);
if (isBeanCandidate(sbd)) {
candidates.add(sbd);
}
}
}
}
return candidates;
}
Filtering Conditions
The scanner determines eligibility through filter logic. By default, the includeFilters are configured to recognize the @Component annotation during the context initialization. The exclusion filters are checked first; if a class matches an exclusion filter, it is rejected. Otherwise, inclusion filters are applied.
protected boolean matchesFilterConditions(MetadataReader reader) {
for (TypeFilter filter : this.excludeFilters) {
if (filter.match(reader, getMetadataReaderFactory())) {
return false;
}
}
for (TypeFilter filter : this.includeFilters) {
if (filter.match(reader, getMetadataReaderFactory())) {
return checkConditionMatch(reader);
}
}
return false;
}
After passing the annotation filter, the candidate undergoes a structural check to ensure it is not an interface or an abstract class (unless the abstract class has methods annotated with @Lookup). It also verifies if the class is independent; non-static inner classes are typically rejected unless they are static.
protected boolean isBeanCandidate(AnnotatedBeanDefinition definition) {
AnnotationMetadata metadata = definition.getMetadata();
return (metadata.isIndependent() &&
(metadata.isConcrete() ||
(metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName()))));
}
Bean Naming and Annotation Processing
Once a candidate is validated, a bean name is generated. If the definition carries a standard annotation (like @Service or @Component) with a value attribute, that value is used. Otherwise, a default name is generated based on the class name, decapitalizing the first character unless the first two characters are uppercase.
Subsequently, common annotations such as @Lazy, @Primary, @DependsOn, @Role, and @Description are parsed and applied to the BeanDefinition. Finally, the definition is registered in the DefaultListableBeanFactory's beanDefinitionMap.
Bean Definition Merging
Before a bean is instantiated, Spring retrieves a merged definition. This step is crucial for handling parent-child bean configurations, which were more common in XML-based setups. The getMergedBeanDefinition method ensures that the final definition combines the parent's attributes with the child's overrides.
protected RootBeanDefinition obtainMergedDefinition(String beanName, BeanDefinition bd) {
synchronized (this.mergedBeanDefinitions) {
RootBeanDefinition mergedDef = this.mergedBeanDefinitions.get(beanName);
if (mergedDef == null || mergedDef.stale) {
if (bd.getParentName() == null) {
mergedDef = (bd instanceof RootBeanDefinition)
? ((RootBeanDefinition) bd).cloneBeanDefinition()
: new RootBeanDefinition(bd);
} else {
BeanDefinition parentDef = getMergedBeanDefinition(bd.getParentName());
mergedDef = new RootBeanDefinition(parentDef);
mergedDef.overrideFrom(bd);
}
if (!StringUtils.hasLength(mergedDef.getScope())) {
mergedDef.setScope(SCOPE_SINGLETON);
}
this.mergedBeanDefinitions.put(beanName, mergedDef);
}
return mergedDef;
}
}
In this merging process, if a parent definition exists, the logic recursively resolves the parent chain. The child definition's properties override those of the parent. For example, if a parent bean is defined as a prototype, but the child definition does not specify a scope, the merge process handles inheritance or defaults to singleton. The resulting RootBeanDefinition represents the complete configuration used for the actual bean creation.