By this point we have:
- A fully built
ApplicationContext(Part 3) -
BeanDefinitions registered by scanning (Part 4) - Auto-configurations loaded (Part 5)
Now refresh() has to turn all those BeanDefinitions into living objects — beans. Let's go through it step by step.
6.1. BeanDefinition — the Bean's Blueprint
Before we talk about the lifecycle, we need to understand what a BeanDefinition is.
public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
String getBeanClassName();
String getScope();
boolean isLazyInit();
boolean isPrimary();
String[] getDependsOn();
boolean isAutowireCandidate();
ConstructorArgumentValues getConstructorArgumentValues();
MutablePropertyValues getPropertyValues();
String getInitMethodName();
String getDestroyMethodName();
int getRole();
// ...
}
A BeanDefinition is metadata about a bean, not the bean itself. From it, Spring learns:
- Which class to instantiate
- Which scope (singleton / prototype / request / session)
- What to inject (constructor args / property values)
- Which init/destroy method to call
- Whether it's lazy or not
The implementation hierarchy:
| Class | When it's used |
|---|---|
RootBeanDefinition |
The main implementation (the merge result) |
ChildBeanDefinition |
Legacy, for parent-child definitions |
GenericBeanDefinition |
The modern one (used in configurations) |
AnnotatedGenericBeanDefinition |
For annotated classes (@Configuration, @Component) |
ScannedGenericBeanDefinition |
For classes found by scanning |
ConfigurationClassBeanDefinition |
For @Bean methods |
A key feature: a BeanDefinition can act as a parent for other BeanDefinitions. When creating a bean, Spring performs a merge — combining parent and child into a RootBeanDefinition. This is called a MergedBeanDefinition.
6.2. The Full Bean Lifecycle — an Overview
Here is the complete scheme. We'll break down every step:
1. BeanDefinition registered (scanning / @Bean / .imports)
2. BeanFactoryPostProcessor.postProcessBeanFactory()
3. BeanPostProcessor registration
4. getBean() is called
5. ─── for singletons ───
6. MergedBeanDefinition is created (parent/child merge)
7. InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation()
8. InstantiationAwareBeanPostProcessor.determineCandidateConstructors()
9. Constructor invoked (or factory method)
10. MergedBeanDefinitionPostProcessor.postProcessMergedBeanDefinition()
11. InstantiationAwareBeanPostProcessor.postProcessAfterInstantiation()
12. InstantiationAwareBeanPostProcessor.postProcessProperties()
└─ @Autowired / @Value / @Resource
13. Aware interfaces: BeanNameAware, BeanClassLoaderAware, BeanFactoryAware
└─ ApplicationContextAware, EnvironmentAware, ...
14. BeanPostProcessor.postProcessBeforeInitialization()
└─ @PostConstruct (CommonAnnotationBeanPostProcessor)
15. InitializingBean.afterPropertiesSet()
16. @Bean(initMethod="...")
17. BeanPostProcessor.postProcessAfterInitialization()
└─ AOP proxy goes here (AbstractAutoProxyCreator)
18. ─── singleton cache ───
19. Bean ready to use
20. ─── at shutdown ───
21. DestructionAwareBeanPostProcessor.postProcessBeforeDestruction()
└─ @PreDestroy
22. DisposableBean.destroy()
23. @Bean(destroyMethod="...")
6.3. BeanFactoryPostProcessor — Working with Metadata
This is the first stage. A BeanFactoryPostProcessor (BFPP) works with BeanDefinitions — before any beans are created.
@FunctionalInterface
public interface BeanFactoryPostProcessor {
void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory);
}
It's invoked by AbstractApplicationContext.invokeBeanFactoryPostProcessors(). The order:
-
BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry()— registers newBeanDefinitions -
BeanFactoryPostProcessor.postProcessBeanFactory()— modifies existing ones
The main BFPP in Spring is ConfigurationClassPostProcessor. It:
- Processes
@Configuration,@Bean,@ComponentScan,@Import,@EnableAutoConfiguration - Registers
BeanDefinitionsfor all the beans it finds
This is where Parts 3–4 take place. ConfigurationClassPostProcessor has the highest precedence (Ordered.HIGHEST_PRECEDENCE), so it runs first.
Your own BFPP:
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
BeanDefinition bd = beanFactory.getBeanDefinition("myService");
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
}
}
Registration: @Component or a @Bean method. Don't inject other beans into it — at this stage they don't exist yet.
6.4. BeanPostProcessor — Working with Beans
A BeanPostProcessor (BPP) is invoked for every bean as it's being created.
public interface BeanPostProcessor {
default Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean;
}
default Object postProcessAfterInitialization(Object bean, String beanName) {
return bean;
}
}
Two methods — before and after initialization. This is where all of Spring's magic is built: @Autowired, @PostConstruct, AOP proxies, @Transactional.
The BPP hierarchy
BeanPostProcessor
├─ InstantiationAwareBeanPostProcessor
│ ├─ postProcessBeforeInstantiation() ← before the bean is created
│ ├─ postProcessAfterInstantiation() ← after creation, before injection
│ └─ postProcessProperties() ← dependency injection
│
├─ SmartInstantiationAwareBeanPostProcessor
│ └─ determineCandidateConstructors() ← constructor selection
│
├─ MergedBeanDefinitionPostProcessor
│ └─ postProcessMergedBeanDefinition() ← collecting metadata for injection
│
└─ DestructionAwareBeanPostProcessor
└─ postProcessBeforeDestruction() ← @PreDestroy
Built-in Spring BPPs
| BPP | What it does | Order |
|---|---|---|
AutowiredAnnotationBeanPostProcessor |
@Autowired, @Value, @Inject
|
Ordered.LOWEST_PRECEDENCE - 2 |
CommonAnnotationBeanPostProcessor |
@PostConstruct, @PreDestroy, @Resource
|
Ordered.LOWEST_PRECEDENCE - 3 |
ConfigurationPropertiesBindingPostProcessor |
@ConfigurationProperties |
Ordered.HIGHEST_PRECEDENCE + 1 |
ApplicationListenerDetector |
Registers ApplicationListener beans |
LOWEST_PRECEDENCE |
AbstractAutoProxyCreator |
AOP proxies (@Transactional, @Async, @Cacheable) |
depends on the implementation |
Important: BPP ordering is critical. For example, @PostConstruct must run before AOP proxying — otherwise the @PostConstruct method would be invoked on the proxy, not on the target object.
6.5. Bean Creation — a Step-by-Step Breakdown
Let's walk through doCreateBean() from AbstractAutowireCapableBeanFactory — the central bean-creation method.
Step 1: createBean() — the entry point
@Override
protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) {
// 1. Give BPPs a chance to return a proxy instead of the real bean
Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
if (bean != null) {
return bean;
}
// 2. Actual creation
return doCreateBean(beanName, mbdToUse, args);
}
Step 2: resolveBeforeInstantiation() — the early proxy
protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {
Object bean = null;
if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) {
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
Class<?> targetType = determineTargetType(beanName, mbd);
bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName);
if (bean != null) {
bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
}
}
}
return bean;
}
InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation() can return a ready-made object — in which case the regular creation cycle is skipped entirely. This is used, for example, for @Configuration proxies and in some AOP scenarios.
Step 3: doCreateBean() — the core logic
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) {
// 1. Instantiate (constructor / factory method)
BeanWrapper instanceWrapper = null;
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
Object bean = instanceWrapper.getWrappedInstance();
// 2. MergedBeanDefinitionPostProcessor
applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
// 3. Early registration (for circular dependencies)
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences
&& isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
// 4. Populate — inject dependencies
Object exposedObject = bean;
populateBean(beanName, mbd, instanceWrapper);
// 5. Initialize
exposedObject = initializeBean(beanName, exposedObject, mbd);
// 6. Circular dependency check
if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
// ...
}
return exposedObject;
}
Step 4: createBeanInstance() — choosing the constructor
protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) {
// 1. If there's a factory method — use it
if (mbd.getFactoryMethodName() != null) {
return instantiateUsingFactoryMethod(beanName, mbd, args);
}
// 2. Determine the constructor
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR
|| mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
return autowireConstructor(beanName, mbd, ctors, args);
}
// 3. The default constructor
return instantiateBean(beanName, mbd);
}
determineConstructorsFromBeanPostProcessors() calls SmartInstantiationAwareBeanPostProcessor.determineCandidateConstructors(). This is where AutowiredAnnotationBeanPostProcessor finds the @Autowired constructor (or the single constructor).
Step 5: populateBean() — injecting dependencies
protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) {
// 1. postProcessAfterInstantiation — a BPP can veto injection
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (InstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().instantiationAware) {
if (!bp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
return;
}
}
}
// 2. postProcessProperties — the injection itself
PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);
if (pvs != null || hasInstantiationAwareBeanPostProcessors()) {
for (InstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().instantiationAware) {
PropertyValues pvsToUse = bp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
// ...
}
}
// 3. applyPropertyValues — setting values (for XML configs)
if (pvs != null) {
applyPropertyValues(beanName, mbd, bw, pvs);
}
}
This is where AutowiredAnnotationBeanPostProcessor.postProcessProperties() finds @Autowired fields and setters and injects them.
Step 6: initializeBean() — initialization
protected Object initializeBean(String beanName, Object bean, RootBeanDefinition mbd) {
// 1. Aware interfaces
invokeAwareMethods(beanName, bean); // BeanNameAware, BeanClassLoaderAware, BeanFactoryAware
// 2. postProcessBeforeInitialization
Object wrappedBean = bean;
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}
// 3. init-method
try {
invokeInitMethods(beanName, wrappedBean, mbd);
} catch (Throwable ex) {
throw new BeanCreationException(...);
}
// 4. postProcessAfterInitialization
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}
return wrappedBean;
}
invokeAwareMethods()
private void invokeAwareMethods(String beanName, Object bean) {
if (bean instanceof Aware) {
if (bean instanceof BeanNameAware bna) {
bna.setBeanName(beanName);
}
if (bean instanceof BeanClassLoaderAware bcla) {
ClassLoader bcl = getBeanClassLoader();
if (bcl != null) bcla.setBeanClassLoader(bcl);
}
if (bean instanceof BeanFactoryAware bfa) {
bfa.setBeanFactory(this);
}
}
}
These are only three of the Aware interfaces. The rest (ApplicationContextAware, EnvironmentAware, ResourceLoaderAware, ApplicationEventPublisherAware, MessageSourceAware) are handled by ApplicationContextAwareProcessor — a BeanPostProcessor registered by the context.
applyBeanPostProcessorsBeforeInitialization()
This is where CommonAnnotationBeanPostProcessor.postProcessBeforeInitialization() runs — the one that invokes @PostConstruct methods.
The ordering of multiple BPPs follows @Order / Ordered. CommonAnnotationBeanPostProcessor has Ordered.LOWEST_PRECEDENCE - 3, so @PostConstruct runs later than other beforeInitialization callbacks but earlier than AOP proxying (which happens in afterInitialization).
invokeInitMethods()
protected void invokeInitMethods(String beanName, Object bean, RootBeanDefinition mbd) {
boolean isInitializingBean = (bean instanceof InitializingBean);
if (isInitializingBean && (mbd == null || !mbd.hasAnyExternallyManagedInitMethod("afterPropertiesSet"))) {
((InitializingBean) bean).afterPropertiesSet();
}
if (mbd != null && bean.getClass() != NullBean.class) {
String initMethodName = mbd.getInitMethodName();
if (StringUtils.hasLength(initMethodName)
&& !(isInitializingBean && "afterPropertiesSet".equals(initMethodName))
&& !mbd.hasAnyExternallyManagedInitMethod(initMethodName)) {
invokeCustomInitMethod(beanName, bean, mbd);
}
}
}
The order: InitializingBean.afterPropertiesSet() → @Bean(initMethod=...). @PostConstruct was already invoked earlier, in postProcessBeforeInitialization.
The final initialization order:
@PostConstructInitializingBean.afterPropertiesSet()@Bean(initMethod = "...")
applyBeanPostProcessorsAfterInitialization()
This is where the AOP proxy is created. AbstractAutoProxyCreator.postProcessAfterInitialization() checks whether the bean carries @Transactional, @Async, @Cacheable, etc., and if so, wraps it in a proxy (CGLIB or a JDK dynamic proxy).
Key point: after afterInitialization, it's the proxy — not the original object — that goes into the singleton cache. All subsequent @Autowired injections will receive the proxy.
6.6. Call Order — a Summary Table
For a MyService bean with the full set of callbacks:
| # | Callback | Invoked by |
|---|---|---|
| 1 | Constructor | createBeanInstance() |
| 2 |
@Autowired fields/setters |
AutowiredAnnotationBeanPostProcessor |
| 3 | BeanNameAware.setBeanName() |
invokeAwareMethods() |
| 4 | BeanFactoryAware.setBeanFactory() |
invokeAwareMethods() |
| 5 | ApplicationContextAware.setApplicationContext() |
ApplicationContextAwareProcessor |
| 6 | @PostConstruct |
CommonAnnotationBeanPostProcessor |
| 7 | InitializingBean.afterPropertiesSet() |
invokeInitMethods() |
| 8 | @Bean(initMethod) |
invokeInitMethods() |
| 9 | AOP proxy | AbstractAutoProxyCreator |
| 10 | Bean ready | Singleton cache |
| 11 | @PreDestroy |
CommonAnnotationBeanPostProcessor |
| 12 | DisposableBean.destroy() |
DisposableBeanAdapter |
| 13 | @Bean(destroyMethod) |
DisposableBeanAdapter |
6.7. Circular Dependencies
The problem:
A → B → A
If A needs B and B needs A — the classic deadlock.
Spring's solution: the three-level cache.
public class DefaultSingletonBeanRegistry {
// Level 1: fully initialized singleton beans
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// Level 2: early singletons (before populate/init)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// Level 3: singleton factories (lambdas producing early references)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
}
The getSingleton() algorithm:
- Check
singletonObjects→ if present, return it. - Check
earlySingletonObjects→ if present, return it. - Check
singletonFactories→ if present, invoke the factory, put the result intoearlySingletonObjects, return it. - Otherwise — create the bean.
An example:
- We start creating
A→ put anObjectFactoryinto level 3. - Injecting
BintoA→ we start creatingB. -
BneedsA→getSingleton("A")→ level 3 → early reference → returns the half-finishedA. -
Bfinishes initializing completely. - We resume initializing
A→Ais ready.
Limitations:
- Works for singletons only.
- Doesn't work with constructor injection — neither bean can be fully created through a constructor, because the constructor runs before the bean is registered in the cache. The fix:
@Lazyon one of the parameters. - Spring Boot 2.6+ bans circular references by default. Re-enable via
spring.main.allow-circular-references=true.
6.8. @lazy — Deferred Initialization
@Service
public class A {
@Autowired
@Lazy
private B b;
}
Spring injects a proxy for B; the real bean is created on first access. This sidesteps circular dependencies.
@Lazy on a @Configuration class makes the entire configuration lazy. @Lazy on a @Bean method makes that bean lazy.
6.9. ObjectProvider and @Lookup
ObjectProvider
@Service
public class MyService {
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public void doWork() {
PrototypeBean bean = prototypeBeanProvider.getObject(); // a new one every time
}
}
An ObjectProvider is a lazy resolver. getObject() asks the container for the bean every time. It works for the prototype scope too.
@Lookup
@Service
public abstract class MyService {
@Lookup
protected abstract PrototypeBean createPrototypeBean();
}
Spring generates a subclass (CGLIB) that overrides the method and asks the container for the bean. An older approach — ObjectProvider is preferred.
6.10. Bean Destruction
registerShutdownHook()
Called from SpringApplication.refreshContext():
private void refreshContext(ConfigurableApplicationContext context) {
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
} catch (AccessControlException ex) { }
}
refresh(context);
}
registerShutdownHook() adds a JVM shutdown hook that will call context.close() when the JVM terminates.
close() → doClose()
protected void doClose() {
// 1. LifecycleProcessor.onClose() — stops Lifecycle beans
if (this.lifecycleProcessor != null) {
this.lifecycleProcessor.onClose();
}
// 2. Destroy the beans
destroyBeans();
// 3. Close the BeanFactory
closeBeanFactory();
// 4. onClose()
onClose();
// 5. Reset the listeners
if (this.earlyApplicationListeners != null) {
this.applicationListeners.clear();
this.applicationListeners.addAll(this.earlyApplicationListeners);
}
// 6. active = false
this.active.set(false);
}
destroyBeans() → destroySingleton()
public void destroySingleton(String beanName) {
removeSingleton(beanName);
DisposableBean disposableBean = this.disposableBeans.remove(beanName);
if (disposableBean != null) {
disposableBean.destroy();
}
}
DisposableBeanAdapter.destroy() calls, in order:
-
DestructionAwareBeanPostProcessor.postProcessBeforeDestruction()→@PreDestroy DisposableBean.destroy()-
@Bean(destroyMethod)orAutoCloseable.close()
Important: Spring automatically calls close() on beans implementing AutoCloseable / Closeable unless you say otherwise. Disable it via @Bean(destroyMethod = "").
6.11. SmartLifecycle and Lifecycle
Lifecycle is an interface for beans with start/stop phases:
public interface Lifecycle {
void start();
void stop();
boolean isRunning();
}
SmartLifecycle extends it:
public interface SmartLifecycle extends Lifecycle, Phased {
boolean isAutoStartup();
void stop(Runnable callback);
int getPhase();
}
The LifecycleProcessor (by default, DefaultLifecycleProcessor) drives:
-
onRefresh()— callsstart()on all beans withisAutoStartup(), ordered bygetPhase()(ascending) -
onClose()— callsstop()on all of them, in descendinggetPhase()order
Example: WebServerStartStopLifecycle — starts/stops Tomcat.
6.12. Spring Boot 3 vs Spring Boot 4
| Aspect | Spring Boot 3 | Spring Boot 4 |
|---|---|---|
| Bean lifecycle | Unchanged | Unchanged |
| BeanPostProcessor | The same interfaces | The same interfaces |
| Circular references | Banned by default (since 2.6) | Banned |
| AOT | Present, but opt-in | More aggressive, reflection-free by default |
@ConstructorBinding |
Optional | Legacy |
BeanRegistrar |
Introduced in Spring Framework 7 | The primary registration approach for AOT |
With AOT (Boot 3.x / 4.x), Spring generates code that registers BeanDefinitions programmatically, without reflection. @PostConstruct, @Autowired, and other callbacks turn into direct code calls. It's faster and compatible with GraalVM native images.
6.13. Diagram: the Complete Bean Lifecycle
BeanDefinition
│
▼
BeanFactoryPostProcessor.postProcessBeanFactory() ← works with metadata
│
▼
BeanPostProcessors registered in the context
│
▼
getBean(beanName)
│
├─ in singletonObjects? → yes → return (already built)
│
▼
createBean()
│
├─ resolveBeforeInstantiation()
│ └─ InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation()
│ └─ if it returned an object → return (shortcut)
│
▼
doCreateBean()
│
├─ 1. createBeanInstance()
│ ├─ determineCandidateConstructors() ← the @Autowired constructor
│ └─ new Instance() / factory method
│
├─ 2. applyMergedBeanDefinitionPostProcessors()
│ └─ @Autowired metadata is collected
│
├─ 3. addSingletonFactory() ← early cache (for circulars)
│
├─ 4. populateBean()
│ ├─ postProcessAfterInstantiation()
│ └─ postProcessProperties() ← @Autowired / @Value
│
├─ 5. initializeBean()
│ ├─ invokeAwareMethods() ← BeanNameAware, BeanFactoryAware
│ ├─ postProcessBeforeInitialization()
│ │ ├─ ApplicationContextAwareProcessor
│ │ └─ CommonAnnotationBeanPostProcessor → @PostConstruct
│ ├─ invokeInitMethods()
│ │ ├─ InitializingBean.afterPropertiesSet()
│ │ └─ @Bean(initMethod)
│ └─ postProcessAfterInitialization()
│ └─ AbstractAutoProxyCreator → AOP proxy
│
├─ 6. addSingleton() ← the finished bean goes into the cache
│
▼
Bean ready
│
▼ (at shutdown)
destroySingleton()
├─ DestructionAwareBeanPostProcessor.postProcessBeforeDestruction()
│ └─ @PreDestroy
├─ DisposableBean.destroy()
└─ @Bean(destroyMethod) / AutoCloseable.close()
6.14. Key Takeaways
- A
BeanDefinitionis metadata, not a bean. From it, Spring learns everything about the future object. - A
BeanFactoryPostProcessorworks with metadata before beans are created. The main BFPP isConfigurationClassPostProcessor. - A
BeanPostProcessorworks with beans. Every bean passes through the BPP chain. - The initialization order:
@PostConstruct→InitializingBean.afterPropertiesSet()→@Bean(initMethod). - The AOP proxy is created in
postProcessAfterInitialization(). After that, the singleton cache holds the proxy, not the original. - Circular dependencies are resolved via the three-level cache — but only for singletons and never for constructor injection.
-
@LazyandObjectProviderare the ways around circular dependencies. - Destruction:
@PreDestroy→DisposableBean.destroy()→@Bean(destroyMethod)→AutoCloseable.close(). -
SmartLifecycleis for beans with start/stop phases (e.g., the web server). - Boot 4 + AOT: the lifecycle turns into generated code, reflection is minimized.
Top comments (0)