Understanding Bean Scopes and Lifecycle in Spring Framework
In Spring Framework, Beans are central to application structure, serving as the primary units managed by the IoC container. This article explores how Bean scopes influence object behavior and how the lifecycle governs their creation and destruction.
Bean Scopes
A Bean’s scope defines its lifecycle and visibility within the Spring container. While Java SE uses "scope" to refer to variable visibility in code blocks, Spring uses it to describe how many instances of a Bean exist and how they are shared.
Spring supports six scopes:
- singleton – One instance per Spring IoC container (default).
- prototype – A new instance is created each time the Bean is requested.
- request – One instance per HTTP request (web-aware contexts only).
- session – One instance per HTTP session (web-aware contexts only).
- application – One instance per ServletContext (web-aware contexts only).
- websocket – One instance per WebSocket session (web-aware contexts only).
Only singleton and prototype are available in non-web Spring applications.
The Problem with Singleton Scope
Consider a scenario where a User object is defined as a singleton Bean:
public class User {
private String name;
private int age;
// getters and setters
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
Registered via:
@Component
public class UserConfig {
@Bean
public User user() {
User u = new User();
u.setName("zhangsan");
u.setAge(20);
return u;
}
}
If one controller modifies this shared instance:
@Controller
public class UserControllerA {
@Autowired
private User user;
public void process() {
User temp = user; // shallow copy
temp.setName("lisi");
temp.setAge(18);
}
}
Another controller retrieving the same Bean will observe the modified state, leading to unexpected behavior:
// Output after both controllers run:
// UserControllerA sees: User{name='zhangsan', age=20}
// After modification: User{name='lisi', age=18}
// UserControllerB sees: User{name='lisi', age=18} ← unintended!
Resolving with Prototype Scope
To avoid shared mutable state, declare the Bean with prototype scope:
@Component
public class UserConfig {
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Bean
public User user() {
User u = new User();
u.setName("zhangsan");
u.setAge(20);
return u;
}
}
Now, each injection point receives a fresh instance:
// UserControllerA modifies its own copy
// UserControllerB receives original values
// Output:
// UserControllerA: User{name='zhangsan', age=20} → modified to lisi/18
// UserControllerB: User{name='zhangsan', age=20}
The @Scope annotation can be applied to either @Bean methods or @Component classes, and values can be specified as strings ("prototype") or constants (ConfigurableBeanFactory.SCOPE_PROTOTYPE).
Spring Execution Flow
- Container Initialization:
ApplicationContextloads configuration (e.g., XML or annotations). - Bean Instantiation: Beans are created via reflection using constructors.
- Dependency Injection: Properties are injected (via setter, field, or constructor injection).
- Registration: Fully initialized Beans are stored in the IoC container (typically a
Map<String, Object>). - Usage: Application code retrieves and uses Beans.
- Destruction: On container shutdown, destruction callbacks are invoked.
Bean Lifecycle
A Bean’s lifecycle consists of five phases:
- Instantiation – Memory allocation and constructor execution.
- Population of Properties – Dependency injection occurs.
- Initialization – In order:
Awareinterface callbacks (e.g.,BeanNameAware.setBeanName())@PostConstructannotated methodInitializingBean.afterPropertiesSet()- Custom
init-methodfrom XML or@Bean(initMethod = "...")
- Usage – The Bean is ready for application logic.
- Destruction – On shutdown:
@PreDestroyannotated methodDisposableBean.destroy()- Custom
destroy-method
Lifecycle Demonstration
@Component
public class LifecycleDemo implements BeanNameAware, InitializingBean {
public LifecycleDemo() {
System.out.println("Constructor called");
}
@Autowired
public void inject(User user) {
System.out.println("Dependency injected");
}
@Override
public void setBeanName(String name) {
System.out.println("BeanNameAware: " + name);
}
@PostConstruct
public void postConstructInit() {
System.out.println("@PostConstruct executed");
}
@Override
public void afterPropertiesSet() {
System.out.println("InitializingBean.afterPropertiesSet()");
}
public void initFromXml() {
System.out.println("XML init-method executed");
}
public void use() {
System.out.println("Using bean");
}
@PreDestroy
public void cleanup() {
System.out.println("@PreDestroy executed");
}
}
With XML configuration:
<bean id="lifecycleDemo"
class="com.example.LifecycleDemo"
init-method="initFromXml" />
<context:component-scan base-package="com.example" />
Output during startup and shutdown:
Constructor called
Dependency injected
BeanNameAware: lifecycleDemo
@PostConstruct executed
InitializingBean.afterPropertiesSet()
XML init-method executed
Using bean
@PreDestroy executed