The Complete Spring Bean Lifecycle: Instantiation, Dependency Injection, Init, and Destroy
Understanding Aware interfaces, BeanPostProcessors, @PostConstruct, and AOP proxying phases.
Part 7 in Series — Catch up on the previous article: Component Scanning Mechanics: Annotations, Metadata Readers, and ASM Bytecode Parsing (Part 6) before diving into this post.
A backend developer creates a database cache manager bean and attempts to initialize a cache warm-up query inside the class constructor:
@Component
public class CacheManager {
@Autowired
private DatabaseClient databaseClient; // Null during constructor execution!
public CacheManager() {
// CONSTRUCTOR EXECUTION:
System.out.println("Warming up cache...");
databaseClient.queryAllRecords(); // Throws NullPointerException!
}
}
When the application boots, the JVM throws a NullPointerException on startup.
The developer is confused: why is databaseClient null when accessed inside the constructor of an @Autowired class?
Because in the Spring Bean Lifecycle, constructor execution occurs BEFORE dependency injection takes place.
Moving the cache warm-up method into a @PostConstruct method solves the issue:
@PostConstruct
public void initCache() {
// Executes AFTER dependency injection completes!
databaseClient.queryAllRecords(); // Runs smoothly!
}
How does Spring transition a raw BeanDefinition recipe into a fully-wired, proxied, production-ready bean?
To avoid subtle initialization bugs, we must trace the 10 Sequential Phases of the Spring Bean Lifecycle.
1. The Complete Bean Lifecycle Flowchart
When ApplicationContext initializes a bean, it executes a strict, multi-stage lifecycle pipeline:
[ 1. BeanDefinition Parsing ]
|
v
[ 2. Instantiation ] -------------------> Constructor executed (Fields are still NULL!)
|
v
[ 3. Populate Properties ] -------------> Dependency Injection (@Autowired fields populated)
|
v
[ 4. Aware Interfaces ] ----------------> BeanNameAware, ApplicationContextAware callbacks
|
v
[ 5. BeanPostProcessor Before ] --------> postProcessBeforeInitialization()
|
v
[ 6. Initialization Callbacks ] --------> @PostConstruct -> InitializingBean -> init-method
|
v
[ 7. BeanPostProcessor After ] ---------> postProcessAfterInitialization() (AOP PROXY WRAPPING!)
|
v
[ 8. READY FOR USE ] -------------------> Application processes requests using bean/proxy
|
v
[ 9. Destruction Callbacks ] -----------> @PreDestroy -> DisposableBean -> destroy-method
2. Detailed Breakdown of Lifecycle Phases
Phase 1: Instantiation (Constructor Execution)
Spring selects an appropriate constructor and invokes newInstance().
- Critical Rule: During constructor execution, NO
@Autowiredfields,@Valueproperties, or dependencies have been injected yet. All fields contain default values (null,0,false).
Phase 2: Populate Properties (Dependency Injection)
Spring uses AutowiredAnnotationBeanPostProcessor to inject references into fields annotated with @Autowired or @Value.
- After this phase completes, all declared dependencies are non-null and accessible.
Phase 3: Aware Interface Callbacks
If the bean implements Spring Aware marker interfaces, Spring injects framework infrastructure references:
BeanNameAware: Passes the bean’s registered string ID (setBeanName).BeanFactoryAware: Passes the owningBeanFactoryinstance.ApplicationContextAware: Passes the activeApplicationContextinstance.
Phase 4: BeanPostProcessor.postProcessBeforeInitialization()
Spring iterates through all registered BeanPostProcessor instances in the container and calls postProcessBeforeInitialization(bean, beanName).
- Common Use Case:
CommonAnnotationBeanPostProcessorscans for@PostConstructannotations and executes annotated methods here!
Phase 5: Initialization Callbacks
Spring executes custom initialization logic across three supported interfaces in order:
@PostConstructMethod: Standard JSR-250 annotation. (Recommended standard).InitializingBean.afterPropertiesSet(): Spring interface callback.- Custom
init-method: Configured via@Bean(initMethod = "customInit").
Phase 6: BeanPostProcessor.postProcessAfterInitialization()
This is one of the most important phases in Spring Core.
- AOP Dynamic Proxy Generation: Frameworks like Spring AOP,
@Transactional, and@Asyncwrap the target bean instance inside a CGLIB or JDK Dynamic Proxy during this phase! - Result: The object returned by
ApplicationContext.getBean()is often not the raw bean instance, but an AOP proxy wrapping the bean!
Phase 7: Ready for Use & Destruction Callbacks
The bean (or proxy) actively serves application requests until the container shuts down (context.close()), triggering destruction callbacks in order:
@PreDestroyMethod: Executes cleanup hooks (closing network sockets, flushing logs).DisposableBean.destroy(): Spring interface destruction callback.- Custom
destroy-method: Configured via@Bean(destroyMethod = "cleanup").
3. Demonstrating Lifecycle Order in Code
@Component
public class LifecycleDemoBean implements BeanNameAware, InitializingBean, DisposableBean {
@Autowired
private DependencyService dependencyService;
// 1. Instantiation
public LifecycleDemoBean() {
System.out.println("1. Constructor Executed (dependencyService = " + dependencyService + ")");
}
// 3. Aware Callback
@Override
public void setBeanName(String name) {
System.out.println("3. BeanNameAware Called (Bean Name: " + name + ")");
}
// 5. @PostConstruct Callback
@PostConstruct
public void postConstruct() {
System.out.println("5. @PostConstruct Executed (dependencyService = " + dependencyService.getClass().getSimpleName() + ")");
}
// 6. InitializingBean Callback
@Override
public void afterPropertiesSet() {
System.out.println("6. InitializingBean.afterPropertiesSet() Executed");
}
// 9. @PreDestroy Callback
@PreDestroy
public void preDestroy() {
System.out.println("9. @PreDestroy Executed");
}
// 10. DisposableBean Callback
@Override
public void destroy() {
System.out.println("10. DisposableBean.destroy() Executed");
}
}
Execution Console Output:
1. Constructor Executed (dependencyService = null)
2. Dependency Injection Completed (@Autowired fields populated)
3. BeanNameAware Called (Bean Name: lifecycleDemoBean)
4. BeanPostProcessor.postProcessBeforeInitialization()
5. @PostConstruct Executed (dependencyService = DependencyService)
6. InitializingBean.afterPropertiesSet() Executed
7. BeanPostProcessor.postProcessAfterInitialization() (Proxy Wrapping)
... [Application Running] ...
9. @PreDestroy Executed
10. DisposableBean.destroy() Executed
Summary & Next Steps
Understanding the Spring Bean Lifecycle prevents initialization bugs and clarifies framework behavior:
- Constructors execute during initial instantiation when
@Autowireddependencies are stillnull. - Property Population injects dependencies into fields after constructor completion.
@PostConstructexecutes after dependency injection completes, serving as the correct location for initialization logic.BeanPostProcessor.postProcessAfterInitialization()wraps target beans inside AOP dynamic proxies for@Transactionaland@Asyncsupport.@PreDestroyexecutes graceful cleanup operations during container shutdown.
In the next article, we open Module 3 with Why Spring Boot Exists: Eliminating XML Configuration and Dependency Hell.
References & Further Reading
- Kiczales, G., et al. (1997). Aspect-Oriented Programming. ECOOP ‘97 Conference Proceedings, Springer.
- ByteBuddy Project. Runtime Code Generation for the Java Virtual Machine. ByteBuddy Docs.
- Spring.io. Aspect Oriented Programming with Spring. Spring Docs.
Part 8: Why Spring Boot Exists: Eliminating XML Configuration and Dependency Hell
Continue to Part 8 →