namalzi

← All posts

Digging into Spring's Source 3: The BeanFactory Hierarchy and BeanDefinition

Spring · Published

Before Spring builds an object it prepares a blueprint for it (BeanDefinition), and it hands objects out through a front desk (BeanFactory) that is split into several small interfaces. This post follows both through real source excerpts and diagrams, written for someone new to Spring.

What this post covers

This is the third post in the "Digging into Spring's Source" series. Post 2 drew the big map of how Spring Framework is divided into modules. This time we step into one of them, spring-beans, and look at the two things that form the skeleton of the container.

What Spring is, and what "container" and "bean" mean, was introduced in post 1. The terms you need are repeated briefly below, so you can start reading here.

The order is:

  1. Why a blueprint (BeanDefinition) has to exist before a bean can be built
  2. What BeanFactory, the front desk you ask for beans, looks like
  3. Why BeanFactory is split into several small interfaces
  4. How DefaultListableBeanFactory, the class that implements all of them, is stacked up
  5. The road one blueprint travels: registered → merged → looked up by type
  6. FactoryBean and &, which confuse almost everyone the first time

The source this post is based on:

Item Value
Project Spring Framework 7.1.0-SNAPSHOT
Commit f447f3c310 (2026-10-01, main branch)
Mostly looking at spring-beans/src/main/java/org/springframework/beans/factory/

Line numbers and implementation details can differ in other versions. Every link under a code block is pinned to this commit, so clicking it shows the same lines as this post. The paths are long, so the link text is shortened to beans/factory/….

In the excerpts, a line that says // ... marks code or comments left out at that spot. Every other line is exactly as in the original.

Five terms to know first

A bean is an object that Spring creates and manages for you. Instead of calling new yourself, you leave the object to Spring, and that object is called a bean.

The container is what creates beans, keeps them, and connects them to each other. Because creating objects is now in the container's hands rather than in your code, it is also called an IoC (Inversion of Control) container. BeanFactory, the subject of this post, is the most basic form of that container.

Dependency injection (DI) means the container finds the other beans a bean needs to do its work and hands them over. If A uses B, A does not build B itself; the container passes B to A.

Scope is the rule for how many of a bean get made. A singleton is made once per container and shared by everyone. A prototype is made fresh every time someone asks. If you say nothing, a bean is a singleton (section 5 shows the exact place where that is decided).

Interfaces and implementing classes are plain Java, but they are the key to this post, so one reminder. An interface is "the list of things you can ask this object to do". One class can implement many interfaces, and which methods you can call on an object depends on the interface type you receive it as. Section 3 is explained entirely by that one property.

1. A bean needs a blueprint first — BeanDefinition

To build a bean, the container has to know quite a lot. Which class is it? Is there one instance or a new one each time? Is it built at start-up or the first time it is used? What goes into the constructor? All of this has to be known before the object exists.

So Spring first creates, for every bean, an object that holds this information. That object is a BeanDefinition. If the bean is the house, the BeanDefinition is the blueprint.

A class marked @Component, a method marked @Bean and a bean tag in XML all become a BeanDefinition. The BeanFactory keeps those blueprints and builds a bean when asked. There are several ways to declare a bean, but inside the container they all take one shape: a BeanDefinition.

Keeping the blueprint separate has some benefits.

BeanDefinition is an interface, and the things a blueprint can record are listed as its methods. Here are a few.

public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
	// ...
	void setBeanClassName(@Nullable String beanClassName);
	// ...
	void setScope(@Nullable String scope);
	// ...
	void setLazyInit(boolean lazyInit);
	// ...
	void setPrimary(boolean primary);
	// ...
}

Source: beans/factory/config/BeanDefinition.java, lines 42–378

@Nullable means "this value is allowed to be null". It shows up everywhere from here on; read it that way and move on.

Grouped by meaning, the information in a blueprint falls into three bundles. The field names are those of AbstractBeanDefinition, the class most blueprint classes inherit from (lines 169–225).

Bundle Fields Meaning
What to build beanClass, factoryMethodName, constructorArgumentValues, propertyValues Which class, with which constructor arguments and property values
How it lives scope, lazyInit, dependsOn, initMethodNames, destroyMethodNames How many, when to build it, and what to call right after building and right before discarding it
As an injection candidate autowireCandidate, primary, fallback Who gets picked first when several beans have the same type

There are three blueprint classes

Three main classes implement BeanDefinition. All three extend AbstractBeanDefinition. How annotations such as @Configuration and @ComponentScan are turned into these blueprints is the subject of Part 8.

Class Where you meet it
GenericBeanDefinition The default for declared blueprints. A bean found by component scanning (the feature that walks through the classes of a package and registers the ones marked @Component and the like) gets its subclass ScannedGenericBeanDefinition
RootBeanDefinition The blueprint of a bean declared with an @Bean method (through the subclass ConfigurationClassBeanDefinition). Also, the merge result you will see in section 5 is always this type
ChildBeanDefinition The old style, with a fixed parent blueprint. Its own class comment says that since Spring 2.5 GenericBeanDefinition effectively supersedes it

For now it is enough to remember: "declarations are mostly the Generic family; what the container actually works with is Root."

Easy to get wrong: a blueprint is neither the bean nor the class

2. The front desk you ask for beans — BeanFactory

The side that keeps the blueprints and hands out beans is BeanFactory. It is the most basic interface, and its heart is getBean.

public interface BeanFactory {
	// ...
	Object getBean(String name) throws BeansException;
	// ...
	<T> T getBean(String name, Class<T> requiredType) throws BeansException;
	// ...
	<T> T getBean(Class<T> requiredType) throws BeansException;
	// ...
	boolean containsBean(String name);
	// ...
}

Source: beans/factory/BeanFactory.java, lines 123–444

Read them and you will see they are all about asking and getting. Give me the bean with this name (getBean(String)), give me the bean of this type (getBean(Class)), is there such a bean (containsBean). The parts left out are the same kind of question: is it a singleton (isSingleton), what is its type (getType). There is not a single method here that registers a bean or changes the container's configuration. That observation is where section 3 starts.

Here is the smallest example of how the blueprint and the front desk fit together. This is not Spring's source but an example written for this post (Greeter is a class of our own; imagine the remaining statements inside some method).

public class Greeter {
    public String hello() { return "hello"; }
}

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();

// 1. Make a blueprint and register it under the name "greeter". No Greeter object exists yet
GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(Greeter.class);
factory.registerBeanDefinition("greeter", definition);

// 2. Ask for the bean by name. Now the container reads the blueprint and builds the object
Greeter greeter = factory.getBean("greeter", Greeter.class);

// 3. No scope was set, so it is a singleton. Asking again returns the same object
Greeter again = factory.getBean("greeter", Greeter.class);   // greeter == again

The example above, step by step. 1. The example code calls registerBeanDefinition("greeter", definition) on DefaultListableBeanFactory; the factory only keeps the "greeter" definition, and no Greeter object exists yet. 2. Calling getBean("greeter", Greeter.class) makes the factory read the definition and create the Greeter object for the first time, which it returns (the subject of Part 4). 3. Calling the same getBean again returns the object already made, because it is a singleton, so again is the same object as greeter Registering a definition and creating the object happen separately. The object is created the first time someone asks for it, and the same object comes back after that.

In a real application you usually do not create a BeanFactory yourself; you use an ApplicationContext (the one from post 2). ApplicationContext also extends interfaces of the BeanFactory family, and its implementation GenericApplicationContext (the parent class of AnnotationConfigApplicationContext, used for annotation-based configuration) holds a DefaultListableBeanFactory in a field. So what this post looks at is the engine of ApplicationContext.

What happens inside a call to getBean (creating the object, injecting dependencies, initialising it) is the subject of post 4. This post stops one step before that: the shape of the front desk, and the blueprint.

3. Why the interface is cut into such small pieces

BeanFactory only had methods for getting things out. So where do you register a blueprint or configure the container? In other interfaces. Spring divides the things you can ask of the container across nine interfaces.

The BeanFactory interface hierarchy. BeanFactory is at the top; HierarchicalBeanFactory, ListableBeanFactory and AutowireCapableBeanFactory extend it. ConfigurableBeanFactory extends HierarchicalBeanFactory and SingletonBeanRegistry, and ConfigurableListableBeanFactory extends three of them at once. BeanDefinitionRegistry only extends AliasRegistry and is not connected to the BeanFactory side. At the bottom, DefaultListableBeanFactory implements both sides. An arrow means "extends (or implements)". Grey is the getting-and-reading side, blue is the configuring side, yellow is the registering side. Notice that the yellow branch on the right is not connected to BeanFactory.

Interface Ability it adds Typical methods
BeanFactory Get a bean by name or type getBean, containsBean, getType
HierarchicalBeanFactory Know the parent factory getParentBeanFactory
ListableBeanFactory Go through the beans as a list instead of asking one by one getBeanDefinitionNames, getBeanNamesForType, getBeansOfType
AutowireCapableBeanFactory Inject into and initialise objects the container did not create autowireBean, initializeBean
SingletonBeanRegistry Put an already-built object in as a singleton registerSingleton
ConfigurableBeanFactory Configure the container itself setParentBeanFactory, registerScope, addBeanPostProcessor
ConfigurableListableBeanFactory Read and edit registered blueprints, finish start-up getBeanDefinition, freezeConfiguration, preInstantiateSingletons
AliasRegistry Manage aliases (other names for the same bean) registerAlias
BeanDefinitionRegistry Add and remove blueprints registerBeanDefinition, removeBeanDefinition

"Parent factory" means containers can be nested as parent and child. When a bean is not found in the child container, the parent is asked.

The reason for the split: the interface you receive is your permission

From the names alone it looks like tidy grouping by role. More precisely, it decides who gets to see how much.

Spring has a number of extension points. You register a bean that implements a particular interface, and the container calls its method at a particular moment. When it does, the container passes itself in as an argument, and the interface type it passes itself as differs from one extension point to another.

Three extension points and the interface each one receives. BeanFactoryAware receives BeanFactory, BeanFactoryPostProcessor receives ConfigurableListableBeanFactory, and BeanDefinitionRegistryPostProcessor receives BeanDefinitionRegistry. It is the same container object, but what you can do with it depends on the type you are handed.

Extension point Method the container calls So what it can do
BeanFactoryAware setBeanFactory(BeanFactory beanFactory) Get beans
BeanFactoryPostProcessor postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) Read and edit registered blueprints
BeanDefinitionRegistryPostProcessor postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) Add and remove blueprints

An ordinary bean only receives a BeanFactory, so it can fetch other beans but cannot change the container. The other two extension points are called at the moment when "all blueprints have been read, but not a single bean has been built yet". That is the last chance to edit or add blueprints, so they receive the interface needed for that job.

In particular, the ability to register a blueprint does not exist anywhere in the BeanFactory family; only BeanDefinitionRegistry has it. The comment on that interface states the intent directly.

This is the only interface in Spring's bean factory packages that encapsulates registration of bean definitions. The standard BeanFactory interfaces only cover access to a fully configured factory instance.

In other words: registering blueprints is the job of this one interface, and the standard BeanFactory interfaces only deal with using a factory whose configuration is already finished (beans/factory/support/BeanDefinitionRegistry.java, lines 29–31). We will read the implementation of its registerBeanDefinition in section 5.

Easy to get wrong

4. The one class that implements all of it — DefaultListableBeanFactory

The class that actually implements the nine interfaces is DefaultListableBeanFactory. It is the class we created with new in the example in section 2.

public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
		implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {

Source: beans/factory/support/DefaultListableBeanFactory.java, lines 132–133

The first two names after implements are the two bottom branches (blue and yellow) of the previous diagram. Implement those two and every interface above them comes along. The third, Serializable, is a standard Java interface and we skip it here.

All that work is not written in a single class. It extends five parent classes in a row, stacked so that each layer owns one job and only the data that job needs.

The inheritance chain of DefaultListableBeanFactory. At the bottom, DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory; above it come AbstractBeanFactory, FactoryBeanRegistrySupport, DefaultSingletonBeanRegistry and SimpleAliasRegistry in turn. As in the previous diagram, an arrow means "extends". Parents are towards the top; the class we actually use is at the bottom. Yellow layers keep objects that have been built, blue layers keep blueprints.

Class What it holds Its job
SimpleAliasRegistry aliasMap Turn an alias into the real name
DefaultSingletonBeanRegistry singletonObjects and others Keep the singleton objects that have been built. The "three-level cache" of post 4 lives in this layer
FactoryBeanRegistrySupport factoryBeanObjectCache Keep objects made by a FactoryBean (section 6)
AbstractBeanFactory mergedBeanDefinitions, scopes, beanPostProcessors The skeleton of getBean; keeps merged blueprints (section 5)
AbstractAutowireCapableBeanFactory A few settings used during creation Actually build the bean and inject its dependencies (createBean)
DefaultListableBeanFactory beanDefinitionMap, beanDefinitionNames The blueprint store; lookup by type

Only SimpleAliasRegistry at the top lives in the spring-core module; the rest are all in the support package of spring-beans.

AbstractBeanFactory does not know where the blueprints are

This is where the design intent of the chain shows most clearly. The skeleton of getBean is in AbstractBeanFactory, yet this class has no blueprint store. When it needs a blueprint it calls the two methods below, and it leaves their bodies empty (abstract).

	protected abstract boolean containsBeanDefinition(String beanName);
	// ...
	protected abstract BeanDefinition getBeanDefinition(String beanName) throws BeansException;

Source: beans/factory/support/AbstractBeanFactory.java, lines 1976–1997

It is saying: "where and how blueprints are stored is for the subclass to decide." A structure where the parent fixes the overall flow and the child fills in the details is called the template method pattern. DefaultListableBeanFactory fills this blank with one map.

	/** Map of bean definition objects, keyed by bean name. */
	private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
	// ...
	/** List of bean definition names, in registration order. */
	private volatile List<String> beanDefinitionNames = new ArrayList<>(256);

Source: beans/factory/support/DefaultListableBeanFactory.java, lines 190–206

So the container's "blueprint cabinet" is, in the end, a map of bean name → BeanDefinition. Yet it also keeps the names in a list. A ConcurrentHashMap is safe when several threads use it at once, but it does not remember the order things were put in. Anything that needs registration order is the list's job (the comment says so too: "in registration order").

5. The road a blueprint travels: registered → merged → looked up by type

Now let us follow one blueprint through the container.

The road a blueprint travels. It is registered through registerBeanDefinition and stored in beanDefinitionMap as the original. getMergedLocalBeanDefinition merges it with its parent into a copy and puts that in mergedBeanDefinitions. getBeanNamesForType and getBean use that merged copy. Yellow is a place where things are kept, blue is an action. The point of this section is that there are two places.

5-1. Registration — registerBeanDefinition

Every blueprint comes in through this one method. It is long, but with only the skeleton left it looks like this.

	@Override
	public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
			throws BeanDefinitionStoreException {

		// ...
		BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
		if (existingDefinition != null) {
			if (!isBeanDefinitionOverridable(beanName)) {
				throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
			}
			// ...
			this.beanDefinitionMap.put(beanName, beanDefinition);
		}
		else {
			// ...
			if (hasBeanCreationStarted()) {
				// ...
			}
			else {
				// Still in startup registration phase
				this.beanDefinitionMap.put(beanName, beanDefinition);
				this.beanDefinitionNames.add(beanName);
				// ...
			}
			// ...
		}

		if (existingDefinition != null || containsSingleton(beanName)) {
			resetBeanDefinition(beanName);
		}
		// ...
	}

Source: beans/factory/support/DefaultListableBeanFactory.java, lines 1251–1332

Read in order:

  1. (In the omitted beginning) it checks that the name is not empty and that the blueprint does not contradict itself.
  2. It looks in the map for an existing blueprint with the same name.
  3. If there is one, it checks whether overriding is allowed. If not, it throws BeanDefinitionOverrideException; if so, it replaces the value in the map with the new blueprint.
  4. If there is none, it puts the blueprint in the map and appends the name to the list.
  5. If an old blueprint was overridden, or a singleton object with the same name already exists, it calls resetBeanDefinition. That method clears away what was made from the old blueprint (the merged copy, and the singleton object already built).

The hasBeanCreationStarted() branch is the case where a blueprint is added after beans have already started being built. Then the whole name list is copied into a new one and swapped in, because some other code may be iterating over that list at the same moment. For now it is enough to know the branch exists.

Easy to get wrong: you might expect registering the same name twice to be an error, but this class allows overriding by default (the comment on setAllowBeanDefinitionOverriding says Default is "true"). The later registration quietly wins. The setting can be turned off with setAllowBeanDefinitionOverriding(false), though, so do not memorise it as "it always gets overridden".

5-2. Merging — what the container really uses is a copy

A blueprint can name a parent (setParentName on BeanDefinition). This is the feature behind the parent attribute in XML configuration: common settings go in the parent blueprint, and the child only writes what differs. To build the bean you need the finished version with parent and child combined, and producing it is called merging.

The thing to notice is that a blueprint with no parent goes through exactly the same step.

				if (bd.getParentName() == null) {
					// Use copy of given root bean definition.
					if (bd instanceof RootBeanDefinition rootBeanDef) {
						mbd = rootBeanDef.cloneBeanDefinition();
					}
					else {
						mbd = new RootBeanDefinition(bd);
					}
				}
				else {
					// Child bean definition: needs to be merged with parent.
					// ...
					// Deep copy with overridden values.
					mbd = new RootBeanDefinition(pbd);
					mbd.overrideFrom(bd);
				}

				// Set default singleton scope, if not configured before.
				if (!StringUtils.hasLength(mbd.getScope())) {
					mbd.setScope(SCOPE_SINGLETON);
				}

Source: beans/factory/support/AbstractBeanFactory.java, lines 1424–1464

bd is the original blueprint, mbd is the merged one (merged bean definition), and pbd is the parent's merged one. bd instanceof RootBeanDefinition rootBeanDef is Java syntax that checks the type and declares a variable in one go. Three things can be read from this code.

The merged blueprint is kept in the mergedBeanDefinitions map of AbstractBeanFactory and reused.

Why bother copying? The original is the object handed to the outside through getBeanDefinition(), and the object a BeanFactoryPostProcessor from section 3 may edit. The container, on the other hand, jots down on the blueprint what it worked out while building the bean (which constructor it decided to use, for example) and reuses those notes next time. If those notes were written on the original, the blueprint the user sees would get cluttered and the two would get tangled. Keeping the original the user sees apart from the merged copy the container uses avoids that.

Easy to get wrong: the original and the merged copy are different objects. If you change the original from getBeanDefinition() after the bean has already been built, the change does not reach the merged copy the container uses, nor the object that already exists. Editing blueprints is supposed to be finished before beans are built, in the BeanFactoryPostProcessor phase. (There are detailed rules for when a merged copy is thrown away and rebuilt; this post does not go into them.)

5-3. Lookup by type — getBeanNamesForType

Looking up by name is fast: take it out of the map. Looking up by type is another matter. The blueprint cabinet is keyed by name, so to answer "which beans have this type?" the container has to walk through every registered name, fetch each merged blueprint and check whether the type matches (doGetBeanNamesForType, lines 619–663). Finding the candidates for injecting a dependency by type ends up going through this lookup too (Part 5 shows how one is picked when there are several).

Walking everything each time is expensive, so it would be nice to remember (cache) the result. But while blueprints are still being added and changed, a remembered result goes stale quickly. So DefaultListableBeanFactory caches type lookups only after freezeConfiguration() has been called (lines 602–617). freezeConfiguration() is the method that declares "the blueprints will not change any more", and ApplicationContext calls it while finishing start-up, right before it builds the singletons ahead of time.

To sum up, the container's start-up is divided into a phase of collecting and editing blueprints and a phase of building beans, and at the boundary the configuration is "frozen". The extension points from section 3 are called in the first phase.

6. FactoryBean and the & prefix

Finally, one thing that trips people up purely because of its name. BeanFactory and FactoryBean look like the same two words swapped, but they are entirely different.

public interface FactoryBean<T> {
	// ...
	@Nullable T getObject() throws Exception;
	// ...
	@Nullable Class<?> getObjectType();
	// ...
	default boolean isSingleton() {
		return true;
	}

}

Source: beans/factory/FactoryBean.java, lines 65–147

It is used when an object is hard to build with just "call the constructor and set some values", so that the building steps can be written out in code. According to the class comment it is used heavily inside the framework itself, and in applications it is common only in infrastructure code. You will rarely write one, but since the framework uses it so much, it is worth knowing.

Below is an example written for this post. It registers, under the name clock, a factory that produces a java.time.Clock (factory is the one from the example in section 2).

public class ClockFactoryBean implements FactoryBean<Clock> {
    @Override
    public Clock getObject() { return Clock.systemUTC(); }

    @Override
    public Class<?> getObjectType() { return Clock.class; }
}

GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(ClockFactoryBean.class);
factory.registerBeanDefinition("clock", definition);

Object a = factory.getBean("clock");    // Clock — the object the factory made
Object b = factory.getBean("&clock");   // ClockFactoryBean — the factory itself

The class written in the blueprint is ClockFactoryBean, yet getBean("clock") returns a Clock. If you need the factory itself, put & in front of the name. That character is defined as a constant on the BeanFactory interface (FACTORY_BEAN_PREFIX, line 132).

Whether getBean is given clock or &clock, it reaches the same fork. If the name does not start with &, it returns the Clock made by the factory's getObject; if it does, it returns the ClockFactoryBean itself. A single & in front of the name separates "what the factory made" from "the factory itself".

This fork exists as-is in getObjectForBeanInstance of AbstractBeanFactory (lines 1854–1895). If the name starts with &, it returns the bean object (the factory) itself. Otherwise an ordinary bean is returned as it is, and for a FactoryBean the result of getObject() is returned.

Easy to get wrong

Recap

Concept One-line summary
BeanDefinition The blueprint of one bean. Neither the bean nor the class
BeanFactory The front desk for getting beans. It only has methods for getting and asking
Nine interfaces Abilities divided up, which is also permissions divided up. The type an extension point receives is its permission
BeanDefinitionRegistry The only interface that registers blueprints. Not connected to the BeanFactory family
DefaultListableBeanFactory The class that implements all nine. It stands on five parent layers, each owning one job
beanDefinitionMap The blueprint cabinet. Bean name → BeanDefinition
Merging Copying the original into a RootBeanDefinition. The container uses this copy
Lookup by type Walks every name and checks. Cached only after the configuration is frozen
FactoryBean A bean that makes another object and offers that. &name is the factory itself

What this post left out

These are in my study notes, but they go too deep for a first read.

What comes next

This post went up to the point just before getBean is called: a blueprint is registered, merged, and can be found.

Post 4: How a Bean Gets Created — the Creation Flow and the Three-Level Cache goes inside getBean. It covers the order in which the merged blueprint is used to build the object, inject its dependencies and initialise it, and why DefaultSingletonBeanRegistry from section 4 keeps singletons in three layers.

More posts