What this post covers
This is the eighth post in the "Digging into Spring's Source" series. Part 7 walked through the twelve steps of refresh(), the method that starts the container, and at the invokeBeanFactoryPostProcessors step it said only "a post-processor runs here that reads configuration classes and adds blueprints". This post follows that post-processor, ConfigurationClassPostProcessor. In the Car/Engine example of Part 1, the moment where @Configuration and @Bean become blueprints is exactly what this post is about.
There are three questions to answer.
- Which code turns a
@Configurationclass and its@Beanmethods intoBeanDefinitions (blueprints)? - How does
@ComponentScanfind classes marked@Component, and why does it do so without loading them? - Why is a
@Configurationclass wrapped with CGLIB, and why does calling a@Beanmethod twice by hand still return the same object?
| Item | Details |
|---|---|
| Source | Spring Framework 7.1.0-SNAPSHOT |
| Commit | f447f3c310 (2026-10-01, main branch) |
| Module read most | spring-context (the context.annotation package) |
| Classes read most | ConfigurationClassPostProcessor, ConfigurationClassParser, ClassPathScanningCandidateComponentProvider, ConfigurationClassBeanDefinitionReader, ConfigurationClassEnhancer |
Every line number and every detail in this post refers to that commit. In other versions the line numbers will differ and some details may too. Part 1 explains how to get the source at the same commit.
My study notes go much deeper, line by line. This post keeps only the main thread that someone seeing Spring for the first time can follow. @Import and @Conditional belong to Part 9, so they appear here only by name, and everything else left out is listed at the end under "If you want to go deeper".
Words to know first
A blueprint (BeanDefinition) is the record of "which class, under which name, built how". It is not the bean itself; it is the information needed to build one. Part 3 covered it.
A configuration class is a class that says what should become a bean. Usually it carries @Configuration and contains @Bean methods. Following the words Spring's source uses, this post also counts a class with only @Component as a configuration class in the wide sense.
A factory method is a method that creates and returns an object instead of new. A @Bean method is a factory method, and the blueprint records it as "this bean is made by calling engine() on the appConfig bean".
Component scanning means walking through the class files of a given package, finding classes marked @Component (or an annotation that carries @Component, such as @Service), and registering them as blueprints.
A meta-annotation is an annotation placed on another annotation. The declaration of @Service carries @Component (Service.java lines 47–48), so Spring treats a class marked @Service as a class that (indirectly) carries @Component. The same goes for @Configuration (Configuration.java lines 437–438).
CGLIB is a library that generates a subclass of a class as bytecode at run time. Spring ships its own copy inside its source (org.springframework.cglib). The generated subclass overrides the parent's methods and can therefore intercept calls to them. It is also one way of building the proxies we met in Part 4.
A BeanFactoryPostProcessor is a post-processor that may change or add blueprints before any bean is created. Part 2, section 6 and Part 6 covered it; the hero of this post, ConfigurationClassPostProcessor, is one of them.
1. The entrance: register() creates exactly one blueprint
Part 1 showed that new AnnotationConfigApplicationContext(AppConfig.class) is really two lines, register(componentClasses) and refresh(). Let us first see what register does.
When an AnnotationConfigApplicationContext is created it prepares two reading tools: AnnotatedBeanDefinitionReader, which takes classes one by one, and ClassPathBeanDefinitionScanner, which walks packages (lines 68–71). register() hands over to the first one (lines 168–175). The real work is in AnnotatedBeanDefinitionReader.doRegisterBean().
private <T> void doRegisterBean(Class<T> beanClass, @Nullable String name,
Class<? extends Annotation> @Nullable [] qualifiers, @Nullable Supplier<T> supplier,
BeanDefinitionCustomizer @Nullable [] customizers) {
AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);
if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
return;
}
abd.setAttribute(ConfigurationClassUtils.CANDIDATE_ATTRIBUTE, Boolean.TRUE);
abd.setInstanceSupplier(supplier);
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
abd.setScope(scopeMetadata.getScopeName());
String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry));
// ...
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);
definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
}
Three things to take from this.
- Only one blueprint is created. A single
AnnotatedGenericBeanDefinitionfor theAppConfigclass, and that is all. The@Beanmethods inside are not read yet. The class is a subclass of theGenericBeanDefinitionfrom Part 3 that also carries the annotation information of the class (AnnotationMetadata) (AnnotatedGenericBeanDefinitionline 45, lines 56–58). - It gets a "candidate" mark.
CANDIDATE_ATTRIBUTEis set totrue. In section 3 this mark means "a class registered directly counts as a configuration-class candidate even without@Configuration". - The name comes from a generator. Unless a name is given,
beanNameGeneratormakes one. The default rule is in section 5.
The post-processor that reads configuration is registered here too
The last line of the AnnotatedBeanDefinitionReader constructor is AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry) (lines 85–91). That method pre-registers the blueprints of the post-processors needed for annotation handling (lines 143–205). Printing what an empty container contains shows them.
// An example written for this post (not Spring's code)
package ex.u06;
// ... (imports omitted)
public class DefaultProcessorsExample {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext()) {
for (String name : context.getBeanDefinitionNames()) {
System.out.println(name + " -> " + context.getBeanDefinition(name).getBeanClassName());
}
}
}
}
org.springframework.context.annotation.internalConfigurationAnnotationProcessor -> org.springframework.context.annotation.ConfigurationClassPostProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor -> org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor -> org.springframework.context.annotation.CommonAnnotationBeanPostProcessor
org.springframework.context.annotation.internalPersistenceAnnotationProcessor -> org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor
org.springframework.context.event.internalEventListenerProcessor -> org.springframework.context.event.EventListenerMethodProcessor
org.springframework.context.event.internalEventListenerFactory -> org.springframework.context.event.DefaultEventListenerFactory
This example was run with the released Spring Framework 7.0.9 and produced the output above (the 7.1.0-SNAPSHOT this series reads was not built and run directly). The registration conditions in the source are these.
| Bean name (constant) | Class | Condition |
|---|---|---|
internalConfigurationAnnotationProcessor |
ConfigurationClassPostProcessor |
always (lines 158–162) |
internalAutowiredAnnotationProcessor |
AutowiredAnnotationBeanPostProcessor |
always (lines 164–168) |
internalCommonAnnotationProcessor |
CommonAnnotationBeanPostProcessor |
when jakarta.annotation.PostConstruct is on the classpath (lines 119–120, lines 171–175) |
internalPersistenceAnnotationProcessor |
PersistenceAnnotationBeanPostProcessor |
when JPA and spring-orm are present (lines 122–124, lines 178–190) |
internalEventListenerProcessor |
EventListenerMethodProcessor |
always (lines 192–196) |
internalEventListenerFactory |
DefaultEventListenerFactory |
always (lines 198–202) |
The example project has JPA and spring-orm, so all six appeared. A project without them gets four or five. All of them are marked with the role ROLE_INFRASTRUCTURE, meaning "a framework-internal bean, not a user bean" (lines 207–212). The first of them, ConfigurationClassPostProcessor, is the rest of this post. The second, which handles @Autowired, is covered in Part 5; the others in Part 6 and Part 12.
* marks the ones registered only when the condition in the table above holds. The yellow boxes are every definition in hand when refresh() starts.
2. ConfigurationClassPostProcessor — a post-processor that reads blueprints to add blueprints
When register() is done, the container holds one blueprint for AppConfig plus a few blueprints for internal post-processors. The @Bean methods become blueprints inside refresh(), at the invokeBeanFactoryPostProcessors step seen in Part 7 (AbstractApplicationContext line 604).
The whole flow of this post. Yellow boxes are where blueprints get made; blue boxes are the post-processor's procedure.
The declaration of ConfigurationClassPostProcessor tells you what it is.
public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor,
BeanRegistrationAotProcessor, BeanFactoryInitializationAotProcessor, PriorityOrdered,
ResourceLoaderAware, ApplicationStartupAware, BeanClassLoaderAware, EnvironmentAware {
BeanDefinitionRegistryPostProcessoris a sub-interface ofBeanFactoryPostProcessor(line 34). On top of the hook for changing blueprints (postProcessBeanFactory) it adds a hook for registering them (postProcessBeanDefinitionRegistry) (line 44). Reading a configuration class produces new blueprints, so this hook is needed.PriorityOrderedmeans "before the other post-processors". The class Javadoc gives the reason: the blueprints of@Beanmethods must be registered before any otherBeanFactoryPostProcessorruns (lines 141–144), because a post-processor the user declared with@Beancan only run once its blueprint exists. And indeedinvokeBeanFactoryPostProcessorscalls thePriorityOrderedBeanDefinitionRegistryPostProcessors first of all (PostProcessorRegistrationDelegatelines 107–118). Within thePriorityOrderedgroup, though, it goes last (getOrder()returnsLOWEST_PRECEDENCE, lines 209–212).
The two hooks lead on like this.
| Hook | Method it calls | What it does | Section |
|---|---|---|---|
postProcessBeanDefinitionRegistry (lines 303–317) |
processConfigBeanDefinitions |
reads configuration classes and registers @Bean and scan results as blueprints |
3–6 |
postProcessBeanFactory (lines 323–339) |
enhanceConfigurationClasses |
swaps the blueprint of a full configuration class for a CGLIB subclass | 7 |
The skeleton of processConfigBeanDefinitions is "find candidates → parse → register blueprints → repeat if new candidates appeared".
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
String[] candidateNames = registry.getBeanDefinitionNames();
for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
// ...
}
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}
// ...
// Parse each @Configuration class
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);
Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = CollectionUtils.newHashSet(configCandidates.size());
do {
StartupStep processConfig = this.applicationStartup.start("spring.context.config-classes.parse");
parser.parse(candidates);
parser.validate();
Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);
// Read the model and create bean definitions based on its content
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
this.reader.loadBeanDefinitions(configClasses);
// ...
candidates.clear();
if (registry.getBeanDefinitionCount() > candidateNames.length) {
// ...
}
}
while (!candidates.isEmpty());
// ...
}
Two objects share the work. ConfigurationClassParser reads a configuration class into an intermediate model called ConfigurationClass (which @Bean methods it has, what it imports, and so on), and ConfigurationClassBeanDefinitionReader looks at that model and registers blueprints. Because reading and registering are separate, if registering produces new candidates — a @Bean method that returns another configuration class, for instance (lines 472–489) — the same loop simply goes round once more.
Easy to get wrong
- The thing that reads blueprints is itself a "bean".
ConfigurationClassPostProcessoris registered as a blueprint in section 1, andinvokeBeanFactoryPostProcessorsfetches it withgetBean()before calling it (PostProcessorRegistrationDelegateline 112). This is why Part 2, section 6 said that collecting definitions and processing definitions are not cleanly separated. - It never runs twice on the same container. Both hooks remember the
identityHashCodeof the registry and throwIllegalStateExceptionif called again (lines 305–314).
3. Full and lite — which blueprints count as a "configuration class"
The first step of the loop, checkConfigurationClassCandidate, looks at one blueprint and decides "do we treat this as a configuration class, and if so is it full or lite", then writes the answer onto the blueprint.
What separates full from lite. Only full gets the CGLIB treatment of section 7.
Map<String, @Nullable Object> config = metadata.getAnnotationAttributes(Configuration.class.getName());
if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) {
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
}
else if (config != null || Boolean.TRUE.equals(beanDef.getAttribute(CANDIDATE_ATTRIBUTE)) ||
isConfigurationCandidate(metadata)) {
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
}
else {
return false;
}
| Verdict | Condition | Meaning |
|---|---|---|
| Not a candidate | no class name, or a factoryMethodName is set (lines 109–112) |
a blueprint made from a @Bean method is not read as a configuration class again |
| full | @Configuration is present and proxyBeanMethods is not false |
replaced by a CGLIB subclass in section 7 |
| lite | @Configuration(proxyBeanMethods = false), a class put in directly via register() (the CANDIDATE_ATTRIBUTE of section 1), or @Component, @ComponentScan, @Import or @ImportResource is present, or there are @Bean methods (lines 79–83, lines 174–189) |
@Bean methods are read, but nothing is wrapped |
The thing to notice is that @Configuration is not required for @Bean methods to be read. A @Bean method inside a @Component class becomes a blueprint too. What @Configuration adds is proxyBeanMethods, that is, the "wrapping" of section 7, and nothing else. The Javadoc of @Configuration says the same: switching that option off is behaviourally equivalent to removing @Configuration (Configuration.java lines 471–477).
The annotation information (metadata) is obtained in one of three ways (lines 114–144). If the blueprint already holds metadata, that is used; if the class is already loaded, reflection reads it; otherwise a MetadataReader reads the class file. That last path is the heart of section 5.
Easy to get wrong: a full class can be downgraded to lite
Even with @Configuration, a non-abstract class that has no instance @Bean methods (all static, or none at all) is changed from full to lite by the parser (ConfigurationClassParser lines 183–189). There is nothing to wrap, so there is no reason to generate a CGLIB subclass.
4. The order in which one configuration class is read
ConfigurationClassParser.parse() makes a ConfigurationClass model per candidate (lines 203–206) and goes through processConfigurationClass() to doProcessConfigurationClass(). If there is a superclass, the method returns it and is called again for it (lines 280–287).
The eight steps for one configuration class. The blue box is where a scan runs on the spot; the yellow box is where @Bean methods are collected.
The comments in the source spell out the order.
| Step | Source comment | Lines | In this post |
|---|---|---|---|
| ① | Recursively process any member (nested) classes first — only if the class carries @Component |
309–312 | nested configuration classes are read before the outer one |
| ② | Process any @PropertySource annotations |
314–325 | Part 12 |
| ③ | The config class is annotated with @ComponentScan -> perform the scan immediately |
327–361 | section 5 |
| ④ | Process any @Import annotations |
363–364 | Part 9 |
| ⑤ | Process any @ImportResource annotations (XML configuration files) |
366–376 | name only |
| ⑥ | Process individual @Bean methods |
378–385 | section 6 |
| ⑦ | Process default methods on interfaces |
387–388, 443–454 | default @Bean methods on interfaces are read too |
| ⑧ | Process superclass, if any — only a parent whose name does not start with java |
390–404 | @Bean methods of a parent class become blueprints too |
③ and ⑥ behave differently. @ComponentScan runs the scan right there and registers blueprints, and if any of the classes found is a configuration-class candidate it is parsed recursively at once (lines 348–359). @Bean methods, on the other hand, are only collected as BeanMethod objects (line 384, ConfigurationClass line 66); turning them into blueprints is done later by the ConfigurationClassBeanDefinitionReader of section 2.
Easy to get wrong
- The order of
@Beanmethods is re-read from the class file. The order in which reflection returns methods can change from run to run, so when there is more than one@Beanmethod the class file is read with ASM to restore declaration order (the comment at lines 462–466). @Conditionalis checked once before parsing starts. The first line ofprocessConfigurationClassisconditionEvaluator.shouldSkip(...)(lines 249–251). If the condition fails, the whole configuration class is skipped. Condition evaluation is the subject of Part 9.
5. @ComponentScan — finding classes without loading them
ComponentScanAnnotationParser.parse(), called from step ③, is mostly a matter of copying the annotation's attributes onto a ClassPathBeanDefinitionScanner (lines 68–129). The packages to walk are basePackages, the packages of basePackageClasses, and if neither is given, the package of the configuration class itself (lines 108–120). The configuration class itself is removed with an exclude filter (lines 122–127); it already has a blueprint and must not be registered again by the scan.
One scan from start to finish. The blue boxes are the part that never loads a class.
The actual scan is ClassPathBeanDefinitionScanner.doScan(), whose first line, findCandidateComponents(), leads to scanCandidateComponents() in the parent class ClassPathScanningCandidateComponentProvider.
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
try {
String packageSearchPattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPattern);
// ...
for (Resource resource : resources) {
// ...
try {
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setSource(resource);
if (isCandidateComponent(sbd)) {
// ...
candidates.add(sbd);
}
// ...
- The package becomes a file pattern.
ex.u06.scanbecomesclasspath*:ex/u06/scan/**/*.class. The default pattern**/*.classis a constant at line 95 (line 95). Sub-packages are included. - A
MetadataReaderis made for every class file. This is what "without loading the class" means. The implementation ofMetadataReaderreads the class file with theClassReaderof a bytecode library called ASM, skipping method bodies (SKIP_CODE,SimpleMetadataReaderlines 36–39). Loading a class into the JVM runs itsstaticinitialisers and drags in the classes it depends on, and you cannot do that to every class in a package. This metadata-reading machinery lives inspring-coreand is covered in Part 13. - The filters are applied.
isCandidateComponent(MetadataReader)rejects the class if any exclude filter matches and accepts it if any include filter matches (lines 533–546). The default include filter is anAnnotationTypeFilterfor@Component, plus one forjakarta.inject.Namedwhen that class is on the classpath (lines 216–227), and that filter looks at meta-annotations by default (AnnotationTypeFilterlines 61–62). That is why@Service,@Repository,@Controllerand@Configurationall match. When an include filter matches,@Conditionalis evaluated on the spot too (lines 554–560, Part 9). - A
ScannedGenericBeanDefinitionis created. The class Part 3 only named. It is a subclass ofGenericBeanDefinition(line 50), and its constructor records only the annotation information theMetadataReaderread and the name of the class (lines 60–63). A string rather than aClassobject: that is the evidence that nothing was loaded. - Is the class independent and concrete?
isCandidateComponent(AnnotatedBeanDefinition)drops inner classes tied to an enclosing instance (nested classes that are notstatic), interfaces and abstract classes (lines 571–575). The exception is an abstract class with@Lookupmethods.
Once the candidates come back, doScan() names and registers them (lines 275–300). Before registering, checkCandidate() looks for an existing blueprint of the same name: if it was registered explicitly, or the two are compatible (the same class scanned twice, for example), the existing one is kept and the new one skipped; if it is a different class that came from a scan, a ConflictingBeanDefinitionException is thrown (lines 338–364). That is the exception you meet when two @Service classes with the same simple name sit in different packages.
The default bean-name rule
Scanned beans and the classes put in with register() in section 1 are both named by AnnotationBeanNameGenerator. If a value was given, as in @Component("name"), that is used (lines 107–117); otherwise the name is built like this.
protected String buildDefaultBeanName(BeanDefinition definition) {
String beanClassName = definition.getBeanClassName();
Assert.state(beanClassName != null, "No bean class name set");
String shortClassName = ClassUtils.getShortName(beanClassName);
return StringUtils.uncapitalizeAsProperty(shortClassName);
}
Drop the package and lower-case the first letter: GreetingService → greetingService. One exception: if the first two letters are both upper case (URLService) the name is left as it is (StringUtils lines 593–599).
Easy to get wrong: a nested class gets its outer class in the name
getShortName() replaces the nested-class separator $ with . (ClassUtils lines 1061–1071). So registering static class AppConfig declared inside Outer gives the bean name outer.AppConfig, not appConfig. The example output in section 6 really does show the name beanDefinitionKindsExample.AppConfig. It looks like a typo the first time, but it is the rule.
6. From a @Bean method to a blueprint
Turning the BeanMethods collected in step ⑥ of section 4 into blueprints is the job of ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod(), called once per @Bean method of each configuration class (lines 147–149).
The three blueprints that come out of one configuration class. A @Bean blueprint records only "whose method to call".
ConfigurationClassBeanDefinition beanDef =
new ConfigurationClassBeanDefinition(configClass, metadata, localBeanName);
beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource()));
// Has this effectively been overridden before (for example, via XML)?
if (isOverriddenByExistingDefinition(beanMethod, beanName, beanDef)) {
// ...
return;
}
if (metadata.isStatic()) {
// static @Bean method
if (configClass.getMetadata() instanceof StandardAnnotationMetadata sam) {
beanDef.setBeanClass(sam.getIntrospectedClass());
}
else {
beanDef.setBeanClassName(configClass.getMetadata().getClassName());
}
beanDef.setUniqueFactoryMethodName(methodName);
}
else {
// instance @Bean method
beanDef.setFactoryBeanName(configClass.getBeanName());
beanDef.setUniqueFactoryMethodName(methodName);
}
- The blueprint type is
ConfigurationClassBeanDefinition, a private nested class that extendsRootBeanDefinitionand implementsAnnotatedBeanDefinition(line 449). This is what the table in Part 3 meant by "@Beanis in the Root family". - An instance method gets
factoryBeanName+factoryMethodName. "Build it by callingengine()on the bean namedappConfig." No bean class is recorded, which is why a@Beanmethod may declare an interface as its return type. - A
staticmethod getsbeanClass+factoryMethodName. It can be called without an instance of the configuration class, so the bean can be built even before the configuration bean exists. That is whystaticis recommended for a@Beanthat returns aBeanFactoryPostProcessor; without it, the CGLIB step of section 7 logs a warning (ConfigurationClassPostProcessorlines 549–557).
Names and aliases. If name (or value, the two are aliases of each other, Bean.java lines 258–271) is given on @Bean, the first value is the bean name and the rest become aliases. If not, the method name is the bean name (lines 208–223, lines 317–319).
Overriding rules. If a blueprint of the same name already exists, isOverriddenByExistingDefinition() decides which one survives (lines 322–380).
| Existing blueprint | Result |
|---|---|
a @Bean method of the same name in the same configuration class (an overload) |
the existing one is kept and marked "more than one method" (lines 333–340) |
| a different method of the same configuration class using the same bean name | BeanDefinitionOverrideException if enforceUniqueMethods of @Configuration (default true) is on or overriding is disabled; otherwise the existing one is kept (lines 341–348) |
| one registered by a component scan | if the return type is that same class, the scanned one is removed quietly and the @Bean one registered (lines 354–359) |
a framework-internal bean (role other than ROLE_APPLICATION) |
the @Bean one overrides it (lines 363–365) |
| anything else (XML, for example) | if overriding is allowed the existing one is kept, otherwise BeanDefinitionOverrideException (lines 369–379) |
Example: the blueprint class depends on how the bean was registered
Let us pull the three kinds of blueprint seen so far out of one container.
// An example written for this post (not Spring's code)
package ex.u06;
// ... (imports omitted)
public class BeanDefinitionKindsExample {
static class Clock {
}
@Configuration
@ComponentScan(basePackageClasses = GreetingService.class) // scans the ex.u06.scan package
static class AppConfig {
@Bean Clock clock() { return new Clock(); }
}
static void show(AnnotationConfigApplicationContext context, String beanName) {
BeanDefinition bd = context.getBeanDefinition(beanName);
System.out.println("'" + beanName + "'");
System.out.println(" blueprint class = " + bd.getClass().getSimpleName());
System.out.println(" beanClassName = " + bd.getBeanClassName());
System.out.println(" factoryBeanName = " + bd.getFactoryBeanName());
System.out.println(" factoryMethodName= " + bd.getFactoryMethodName());
}
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
show(context, "beanDefinitionKindsExample.AppConfig"); // the config class put in with register()
show(context, "greetingService"); // found by @ComponentScan
show(context, "clock"); // a @Bean method
}
}
}
ex.u06.scan.GreetingService is an empty class marked only @Service. The output (the labels were printed in Korean in the original run; translated here):
'beanDefinitionKindsExample.AppConfig'
blueprint class = AnnotatedGenericBeanDefinition
beanClassName = ex.u06.BeanDefinitionKindsExample$AppConfig$$SpringCGLIB$$0
factoryBeanName = null
factoryMethodName= null
'greetingService'
blueprint class = ScannedGenericBeanDefinition
beanClassName = ex.u06.scan.GreetingService
factoryBeanName = null
factoryMethodName= null
'clock'
blueprint class = ConfigurationClassBeanDefinition
beanClassName = null
factoryBeanName = beanDefinitionKindsExample.AppConfig
factoryMethodName= clock
This example was run with the released Spring Framework 7.0.9 and produced the values above. The three entries match three sections: what register() put in is an AnnotatedGenericBeanDefinition (section 1), what the scan found is a ScannedGenericBeanDefinition (section 5), and the @Bean method is a ConfigurationClassBeanDefinition (section 6). That clock has a null beanClassName and a filled-in factoryBeanName instead, and that the configuration class's beanClassName ends in $$SpringCGLIB$$0, is the story of the next section.
7. Why CGLIB? — why calling a @Bean method twice gives the same object
Let us create the problem first. In Part 1, car(Engine engine) took its engine as an argument, but configuration that calls the method directly is common too.
// An example written for this post (not Spring's code)
@Configuration // proxyBeanMethods defaults to true
static class FullConfig {
@Bean Engine engine() { return new Engine(); }
@Bean Car car() { return new Car(engine()); } // calls engine() directly
}
In plain Java the container would call engine() once (to build the engine bean) and car() would call it again, so there would be two Engines. The singleton would be broken. Spring prevents this by generating a subclass of the configuration class that intercepts the @Bean methods.
The wrapping side: enhanceConfigurationClasses
enhanceConfigurationClasses(), called from the second hook postProcessBeanFactory of section 2, picks out only the blueprints marked full in section 3 (lines 544–561) and swaps the class in each blueprint.
ConfigurationClassEnhancer enhancer = new ConfigurationClassEnhancer();
for (Map.Entry<String, AbstractBeanDefinition> entry : configBeanDefs.entrySet()) {
AbstractBeanDefinition beanDef = entry.getValue();
// If a @Configuration class gets proxied, always proxy the target class
beanDef.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
// Set enhanced subclass of the user-specified bean class
Class<?> configClass = beanDef.getBeanClass();
Class<?> enhancedClass = enhancer.enhance(configClass, this.beanClassLoader);
if (configClass != enhancedClass) {
// ...
beanDef.setBeanClass(enhancedClass);
}
}
The configuration object does not exist yet. Only the beanClass of the blueprint changes, from AppConfig to the subclass CGLIB generated. So when the container later builds the appConfig bean, it is the subclass's constructor that runs, and that is why the beanClassName in the section 6 example ended in $$SpringCGLIB$$0. That tag is added by SpringNamingPolicy (line 35).
ConfigurationClassEnhancer.newEnhancer() decides what the subclass looks like (lines 171–189).
- The parent is the configuration class and the interface it implements is
EnhancedConfiguration(lines 180–181). That is a marker interface extendingBeanFactoryAware(lines 227–228); it keeps an already-wrapped class from being wrapped again (lines 103–113) and makes the container callsetBeanFactory()on it. - The subclass gets one extra
publicfield named$$beanFactory(line 89, lines 287–297). The intercepted methods need a way to reach the container. - There are three callbacks (lines 81–85).
BeanMethodInterceptorintercepts@Beanmethods (lines 470–475),BeanFactoryAwareMethodInterceptorinterceptssetBeanFactory()and stores the container in the field above (lines 306–320), and every other method getsNoOp, that is, it is left alone.setBeanFactory()is called byinvokeAwareMethodsin the initialisation step we saw in Part 4 (AbstractAutowireCapableBeanFactorylines 1836–1848).
The intercepting side: BeanMethodInterceptor
From now on, every call to a @Bean method reaches BeanMethodInterceptor.intercept() first. It asks one question: "Is this call the container trying to build the bean, or is it user code?"
The fork after interception. The yellow box is the "fetch it from the container" path.
if (isCurrentlyInvokedFactoryMethod(beanMethod)) {
// The factory is calling the bean method in order to instantiate and register the bean
// (i.e. via a getBean() call) -> invoke the super implementation of the method to actually
// create the bean instance.
// ...
return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs);
}
return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName);
}
- If the container called it,
invokeSuper. The parent's method, the body we wrote, runs and doesnew Engine(). This is the only path on which the bean is really created. - Otherwise,
resolveBeanReference. The bean name is derived from the method (BeanAnnotationHelper.determineBeanNameFor, line 355) and the bean is fetched from the container withbeanFactory.getBean(beanName)(lines 428–429). As Part 4 showed,getBean()returns the cached object if there is one and builds it only otherwise. Building it callsengine()again, but that time the call comes from the container and takes theinvokeSuperpath above.
How does it know whether the container is calling? Just before invoking a factory method, the container writes that Method into a thread-local and restores the previous value afterwards (SimpleInstantiationStrategy line 47, lines 68–82). The interceptor compares that value with the name and parameter types of the method it just intercepted (lines 510–514). While car is in the thread-local because car() is being built, a call to engine() has a different name and is therefore judged to be user code.
Example: turn proxyBeanMethods off and you get two objects
// An example written for this post (not Spring's code)
package ex.u06;
// ... (imports omitted)
public class ProxyBeanMethodsExample {
static class Engine {
static int created = 0; // counts constructor calls
Engine() { created++; }
}
static class Car {
final Engine engine;
Car(Engine engine) { this.engine = engine; }
}
@Configuration // proxyBeanMethods defaults to true
static class FullConfig {
@Bean Engine engine() { return new Engine(); }
@Bean Car car() { return new Car(engine()); } // calls engine() directly
}
@Configuration(proxyBeanMethods = false) // not wrapped with CGLIB
static class LiteConfig {
@Bean Engine engine() { return new Engine(); }
@Bean Car car() { return new Car(engine()); }
}
static void run(Class<?> configClass) {
Engine.created = 0;
try (var context = new AnnotationConfigApplicationContext(configClass)) {
Object config = context.getBean(configClass);
Car car = context.getBean(Car.class);
Engine engine = context.getBean(Engine.class);
System.out.println("[" + configClass.getSimpleName() + "]");
System.out.println(" actual class of the config object = " + config.getClass().getName());
System.out.println(" car.engine == engine bean ? " + (car.engine == engine));
System.out.println(" Engine constructor calls = " + Engine.created);
}
}
public static void main(String[] args) {
run(FullConfig.class);
run(LiteConfig.class);
}
}
[FullConfig]
actual class of the config object = ex.u06.ProxyBeanMethodsExample$FullConfig$$SpringCGLIB$$0
car.engine == engine bean ? true
Engine constructor calls = 1
[LiteConfig]
actual class of the config object = ex.u06.ProxyBeanMethodsExample$LiteConfig
car.engine == engine bean ? false
Engine constructor calls = 2
This example was run with the released Spring Framework 7.0.9 and produced the values above (the labels were printed in Korean in the original run; translated here). With FullConfig, the actual class of the configuration object is the CGLIB subclass, the engine() called inside car() is the same object fetched from the container, and the constructor ran once. LiteConfig, with proxyBeanMethods = false, became lite in the sense of section 3 and was not wrapped; engine() was an ordinary method call, so there were two Engines. As the Javadoc of @Configuration says, if you do not want the wrapping, each @Bean method must be a self-contained factory method that does not call the others — taking what it needs as arguments is the way to do that (lines 458–470).
Easy to get wrong
finalis not allowed. A subclass has to be generated, so a full configuration class cannot befinal, and its instance@Beanmethods must be overridable. The parser's validation step reports both as errors (ConfigurationClasslines 248–252,BeanMethodlines 66–71). WithproxyBeanMethods = falsethe restriction goes away.- A configuration object created too early cannot be wrapped. If the
appConfigsingleton already exists whenenhanceConfigurationClassesruns (because an instance@Beanmethod returns aBeanFactoryPostProcessor, say), only a warning is logged (lines 549–557). The fix the warning recommends is thestaticof section 6. - What is wrapped is the configuration class, not the beans the
@Beanmethods make.EngineandCarare ordinary objects. CGLIB only touchesFullConfig. This is a subclass used in a different place and for a different purpose than the AOP proxies of Part 4.
If you want to go deeper
A lot was left out to keep the main thread visible. The study notes cover these; here they are only named. Registering a configuration class brought in by @Import (registerBeanDefinitionForImportedConfigurationClass, lines 159–179) and the ImportAwareBeanPostProcessor registered for ImportAware (ConfigurationClassPostProcessor line 338). Copying the initMethod, destroyMethod, autowireCandidate and bootstrap attributes of @Bean onto the blueprint (lines 266–287). Scoped proxies (applyScopedProxyMode) and the special handling in the interceptor when a @Bean method returns a FactoryBean (lines 368–382). Reading a component index (a spring.components file) instead of scanning when one exists (ClassPathScanningCandidateComponentProvider lines 312–322). And the ahead-of-time processing (processAheadOfTime, lines 342–357), which Part 16 covers.
Recap
| Concept | In one line |
|---|---|
register() |
One AnnotatedGenericBeanDefinition per class passed in. @Bean is not read yet |
| Default post-processors | Up to six are registered when AnnotatedBeanDefinitionReader is created. The first is ConfigurationClassPostProcessor |
ConfigurationClassPostProcessor |
A BeanDefinitionRegistryPostProcessor that reads blueprints to add blueprints. PriorityOrdered, so it runs before the other post-processors |
| full / lite | @Configuration with proxyBeanMethods not switched off is full. Other @Component classes and classes with @Bean methods are lite. @Bean is read in both cases |
| Reading order | member classes → @PropertySource → @ComponentScan → @Import → @ImportResource → @Bean → default methods on interfaces → superclass |
| Component scan | Finds **/*.class and reads only the annotations with ASM. No class is loaded. The result is a ScannedGenericBeanDefinition |
| Default name | The @Component value, else the short class name with its first letter lower-cased. A nested class becomes outer.Inner |
@Bean blueprint |
ConfigurationClassBeanDefinition. factoryBeanName + factoryMethodName (instance) or beanClass + factoryMethodName (static). Named by name, else the method name |
| CGLIB wrapping | The beanClass of a full configuration class's blueprint is swapped for a subclass. Its name carries $$SpringCGLIB$$ |
BeanMethodInterceptor |
If the container made the call, the original body runs; if user code did, getBean() is used. So calling twice gives the same object |
proxyBeanMethods = false |
No wrapping. Calling a @Bean method directly creates a new object |
What to look at next
Several places in this post were deferred with "skipped if the condition fails" or "@Import is in Part 9". The next post takes those two up: at what moment @Conditional and @Profile look at which information to decide whether a blueprint is registered, and how @Import pulls in other configuration classes, ImportSelectors and ImportBeanDefinitionRegistrars — all inside the same ConfigurationClassParser.
- Next: Digging into Spring's Source 9: @Conditional, @Profile and @Import — Choosing Beans by Condition
- Previous: Part 7: The Twelve Steps of refresh() — the Order in Which the Container Starts
- For the blueprint itself: Part 3: The BeanFactory Hierarchy and BeanDefinition
- For how an object is built from a blueprint: Part 4: How a Bean Gets Created — the Creation Flow and the Three-Level Cache
- Where the series starts: Part 1: How I Read It with AI, and the Map