namalzi

← All posts

Digging into Spring's Source 8: How @Configuration and @ComponentScan Become Blueprints

Spring · Published

How a configuration class (@Configuration), its @Bean methods, and the @Component classes found by @ComponentScan turn into bean blueprints (BeanDefinition), followed step by step for someone new to Spring. How ConfigurationClassPostProcessor reads blueprints to add more blueprints, how component scanning reads class files without loading them, and why a @Configuration class is wrapped with CGLIB so that calling a @Bean method twice still gives you the same object — all checked against the real source and runnable examples.

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.

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);
	}

spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java, lines 252–293

Three things to take from this.

  1. Only one blueprint is created. A single AnnotatedGenericBeanDefinition for the AppConfig class, and that is all. The @Bean methods inside are not read yet. The class is a subclass of the GenericBeanDefinition from Part 3 that also carries the annotation information of the class (AnnotationMetadata) (AnnotatedGenericBeanDefinition line 45, lines 56–58).
  2. It gets a "candidate" mark. CANDIDATE_ATTRIBUTE is set to true. In section 3 this mark means "a class registered directly counts as a configuration-class candidate even without @Configuration".
  3. The name comes from a generator. Unless a name is given, beanNameGenerator makes 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.

What happens before refresh(). ① The AnnotationConfigApplicationContext constructor creates an AnnotatedBeanDefinitionReader, which calls registerAnnotationConfigProcessors() to put in the post-processor definitions (ROLE_INFRASTRUCTURE): ConfigurationClassPostProcessor, AutowiredAnnotationBeanPostProcessor, CommonAnnotationBeanPostProcessor (conditional), PersistenceAnnotationBeanPostProcessor (conditional), EventListenerMethodProcessor and DefaultEventListenerFactory. It then creates a ClassPathBeanDefinitionScanner for scanning packages. ② register(AppConfig.class) goes through reader.doRegisterBean(), sets CANDIDATE_ATTRIBUTE to true and registers one AnnotatedGenericBeanDefinition for AppConfig under the name chosen by beanNameGenerator. ③ So just before refresh(), the BeanFactory holds the post-processor definitions plus one AppConfig definition, and the @Bean methods have not been read yet * 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).

register creates one AnnotatedGenericBeanDefinition; at the invokeBeanFactoryPostProcessors step of refresh, postProcessBeanDefinitionRegistry of ConfigurationClassPostProcessor has ConfigurationClassParser read the configuration class and ConfigurationClassBeanDefinitionReader register one blueprint per @Bean method; then postProcessBeanFactory's enhanceConfigurationClasses swaps full configuration classes for CGLIB subclasses; finally finishBeanFactoryInitialization builds the beans. 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 {

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java, lines 152–154

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());
		// ...
	}

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java, lines 389–506

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

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.

The decision order of checkConfigurationClassCandidate. No class name or a factoryMethodName means not a candidate. @Configuration with proxyBeanMethods not false means full. Otherwise, @Configuration with proxyBeanMethods=false, a class registered directly via register, the @Component family, @Import, @ImportResource, or @Bean methods means lite. Anything else is not a candidate. 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;
		}

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java, lines 146–156

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 order in which doProcessConfigurationClass reads one configuration class: member classes, @PropertySource, @ComponentScan, @Import, @ImportResource, @Bean methods, default methods on interfaces, and then the same order again for a superclass whose name does not start with java. 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

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.

The flow of a component scan. The package is turned into a classpath pattern and every class file is found; a MetadataReader reads annotations from bytecode without loading the class; exclude and include filters are applied; conditions and the independent-and-concrete check follow; a ScannedGenericBeanDefinition is created, named and registered. 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);
						}
						// ...

spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java, lines 446–473

  1. The package becomes a file pattern. ex.u06.scan becomes classpath*:ex/u06/scan/**/*.class. The default pattern **/*.class is a constant at line 95 (line 95). Sub-packages are included.
  2. A MetadataReader is made for every class file. This is what "without loading the class" means. The implementation of MetadataReader reads the class file with the ClassReader of a bytecode library called ASM, skipping method bodies (SKIP_CODE, SimpleMetadataReader lines 36–39). Loading a class into the JVM runs its static initialisers and drags in the classes it depends on, and you cannot do that to every class in a package. This metadata-reading machinery lives in spring-core and is covered in Part 13.
  3. 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 an AnnotationTypeFilter for @Component, plus one for jakarta.inject.Named when that class is on the classpath (lines 216–227), and that filter looks at meta-annotations by default (AnnotationTypeFilter lines 61–62). That is why @Service, @Repository, @Controller and @Configuration all match. When an include filter matches, @Conditional is evaluated on the spot too (lines 554–560, Part 9).
  4. A ScannedGenericBeanDefinition is created. The class Part 3 only named. It is a subclass of GenericBeanDefinition (line 50), and its constructor records only the annotation information the MetadataReader read and the name of the class (lines 60–63). A string rather than a Class object: that is the evidence that nothing was loaded.
  5. Is the class independent and concrete? isCandidateComponent(AnnotatedBeanDefinition) drops inner classes tied to an enclosing instance (nested classes that are not static), interfaces and abstract classes (lines 571–575). The exception is an abstract class with @Lookup methods.

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);
	}

spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java, lines 244–249

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 AppConfig class becomes an AnnotatedGenericBeanDefinition via register, and its engine and car methods are registered by ConfigurationClassBeanDefinitionReader as ConfigurationClassBeanDefinitions whose factoryBeanName is appConfig and whose factoryMethodName is engine or car. To build the bean, that method is called on the appConfig bean. 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);
		}

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java, lines 225–253

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);
			}
		}

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java, lines 569–584

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 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?"

When a call to engine arrives, BeanMethodInterceptor intercepts it and works out the bean name. If the container is calling this very method as a factory method right now, invokeSuper runs the original body and creates a new Engine; otherwise beanFactory.getBean fetches it from the container, returning the same object if cached. 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);
		}

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java, lines 384–402

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

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.

More posts