Spring IOC & Bean Lifecycle
IOC (Inversion of Control) delegates object creation and dependency injection to the container. The bean lifecycle includes instantiation, dependency injection, initialization, use, and destruction. This article explains IOC, lifecycle phases, circular dependency resolution, and extension points with a reference table.
Overview
- IOC: The application does not
newobjects; the Spring container creates them and injects dependencies. Dependencies are declared via @Autowired, constructor injection, setter injection, or @Resource. - Lifecycle: Instantiate → property injection → Aware callbacks → BeanPostProcessor before init → @PostConstruct / InitializingBean / init-method → BeanPostProcessor after init → bean ready → @PreDestroy / DisposableBean / destroy-method.
- Scope: singleton (default, one per context), prototype (new instance per getBean), request, session. Scope affects when beans are created and destroyed.
Example
Example 1: Dependency injection styles
Java@Service public class OrderService { @Autowired private UserService userService; // Constructor injection (preferred for required deps) public OrderService(UserService userService) { this.userService = userService; } // Setter injection (for optional deps) @Autowired(required = false) public void setAuditService(AuditService auditService) { ... } }
- Constructor injection is preferred: dependencies are explicit, immutable, and test-friendly. Field injection is concise but harder to test and hides dependencies.
Example 2: Lifecycle hooks
Java@PostConstruct public void init() { // Runs after property injection, before bean is fully ready } @PreDestroy public void destroy() { // Runs when context is shutting down } // Alternative: implement InitializingBean, DisposableBean
- @PostConstruct and @PreDestroy are standard (JSR 250). InitializingBean and DisposableBean are Spring-specific. init-method and destroy-method can be specified in config.
Example 3: Phase order
| Phase | Description |
|---|---|
| Instantiation | Constructor called |
| Property injection | @Autowired, @Value, etc. |
| Aware callbacks | BeanNameAware, ApplicationContextAware, etc. |
| BeanPostProcessor before | postProcessBeforeInitialization |
| Initialization | @PostConstruct, InitializingBean.afterPropertiesSet(), init-method |
| BeanPostProcessor after | postProcessAfterInitialization (AOP proxies created here) |
| Use | Bean ready for use |
| Destroy | @PreDestroy, DisposableBean.destroy(), destroy-method |
Example 4: Scope
| Scope | Creation | Destruction |
|---|---|---|
| singleton | Once per context | On context close |
| prototype | Each getBean() | Never (caller owns lifecycle) |
| request | Per HTTP request | End of request |
| session | Per HTTP session | End of session |
- Prototype beans are not cached; each getBean returns a new instance. Destruction callbacks are not called for prototype-scoped beans.
Example 5: Aware interfaces
Java@Component public class MyBean implements ApplicationContextAware, BeanNameAware { @Override public void setApplicationContext(ApplicationContext ctx) { // Access other beans, environment, etc. } @Override public void setBeanName(String name) { // Bean name in the container } }
- Use when a bean needs to know its name or access the context. Prefer dependency injection over Aware when possible.
Example 6: BeanPostProcessor
Java@Component public class MyBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String beanName) { // Can wrap or replace bean (e.g. for AOP proxy) return bean; } }
- BeanPostProcessors run for every bean. AOP proxy creation, @Autowired processing, and validation use BeanPostProcessors. Order matters; use @Order to control.
Core Mechanism / Behavior
- IOC: Container holds bean definitions (from @Component scan, @Bean, etc.), creates instances, resolves dependencies (by type and name), and manages lifecycle.
- Circular dependency: Constructor injection cannot resolve cycles. Field/setter injection can: Spring uses a three-level cache. When A needs B and B needs A, Spring creates a half-initialized A, injects it into B, finishes B, then injects B into A. Works only for singleton beans.
- BeanPostProcessor: Extensions run before and after initialization. Used by AOP (proxy creation), validation, CommonAnnotationBeanPostProcessor (@PostConstruct, @Resource), etc.
Circular Dependency Resolution
- Three-level cache: singletonObjects (fully initialized), earlySingletonObjects (early reference), singletonFactories (factory for early reference).
- When a circular dependency is detected, the factory creates an early proxy (or raw instance) so the dependent can be constructed. After both are done, the proxy is replaced with the final bean if needed.
- Limitation: Only works with singleton scope and when the cycle does not involve constructor injection. If A and B both use constructor injection for each other, resolution fails.
Why Constructor Injection is Preferred
Constructor injection makes dependencies explicit and required. The class cannot be instantiated without them, which catches configuration errors early. Dependencies can be final, supporting immutability. Testing is straightforward: pass mocks via the constructor. Field injection hides dependencies and makes testing harder, as you must use reflection or a framework to inject mocks. Setter injection is useful for optional dependencies or when circular dependencies force you to break the cycle with a setter. As a default, use constructor injection for required dependencies; reserve field and setter injection for special cases.
Bean Definition Sources
Bean definitions come from several sources. Component scanning finds classes annotated with @Component, @Service, @Repository, @Controller, and registers them. @Bean methods in @Configuration classes define beans programmatically. @Import brings in additional configuration. XML (legacy) and other metadata also contribute. The container merges all sources and builds a unified bean definition registry. When multiple definitions exist for the same bean name, the last one typically wins, but this can vary by source order and configuration. Understanding where your beans come from helps debug missing or duplicate beans.
Key Rules
- Prefer constructor injection for required dependencies; use @Autowired(required=false) for optional.
- Circular dependency: Avoid when possible; if needed, use setter/field injection. Constructor injection cannot participate in cycles.
- Understanding the lifecycle helps with @PostConstruct, BeanPostProcessor, and AOP proxy creation order.
- Prototype beans are not managed after creation; the container does not call destroy for them.
Lazy Initialization
By default, singleton beans are created at startup. With lazy initialization (spring.main.lazy-initialization=true or @Lazy on a bean), creation is deferred until the bean is first requested. This can speed up startup when many beans are not needed immediately, but shifts cost to the first request and can mask startup problems. Use for beans that are rarely used or expensive to create; avoid for critical path beans where you want to fail fast at startup.
What's Next
See Spring AOP, Spring Boot Startup. See Transaction Management for proxy and lifecycle interaction.