Spring Bean Lifecycle
Introduction
The Spring Framework manages beans through a well-defined lifecycle, which is one of its core strengths. Understanding this lifecycle helps developers properly initialize resources, perform actions after dependencies are injected, and release resources when beans are no longer needed.
In this tutorial, we'll explore the complete lifecycle of a Spring bean from creation to destruction, learn about the various callback methods, and see practical examples of how to leverage these lifecycle hooks in real applications.
Spring Bean Lifecycle Overview
The lifecycle of a Spring bean consists of several distinct phases:
- Bean Instantiation - Spring creates an instance of the bean
- Dependency Injection - Properties and dependencies are set
- Bean Post-Processing - Pre-Initialization - BeanPostProcessors can modify beans before initialization
- Bean Initialization - Custom initialization methods are called
- Bean Post-Processing - Post-Initialization - BeanPostProcessors can modify beans after initialization
- Bean is Ready for Use - The bean is used by the application
- Bean Destruction - Cleanup methods are called when the container is closed
Let's dive deeper into each of these phases.
Bean Instantiation and Dependency Injection
When Spring creates a bean, it first instantiates it using a constructor and then injects dependencies by setting properties.
public class UserService {
private UserRepository userRepository;
// Constructor injection
public UserService(UserRepository userRepository) {
System.out.println("UserService constructor called");
this.userRepository = userRepository;
}
// Or setter injection
public void setUserRepository(UserRepository userRepository) {
System.out.println("Setter injection performed");
this.userRepository = userRepository;
}
}
Bean Initialization Callbacks
Spring provides several ways to perform initialization tasks after a bean has been created and its dependencies injected:
1. Implementing InitializingBean Interface
import org.springframework.beans.factory.InitializingBean;
public class UserService implements InitializingBean {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
System.out.println("UserService constructor called");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("InitializingBean's afterPropertiesSet method called");
// Perform initialization logic
if (userRepository == null) {
throw new IllegalArgumentException("UserRepository must be set");
}
}
}
2. Using @PostConstruct Annotation
import javax.annotation.PostConstruct;
public class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
System.out.println("UserService constructor called");
}
@PostConstruct
public void init() {
System.out.println("@PostConstruct method called");
// Perform initialization logic
validateDependencies();
loadInitialData();
}
private void validateDependencies() {
if (userRepository == null) {
throw new IllegalArgumentException("UserRepository must be set");
}
}
private void loadInitialData() {
// Load some initial data or perform startup tasks
}
}
3. Using init-method Attribute in XML Configuration
public class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
System.out.println("UserService constructor called");
}
public void initMethod() {
System.out.println("Custom init method called");
// Initialization logic
}
}
XML configuration:
<bean id="userService" class="com.example.UserService" init-method="initMethod">
<constructor-arg ref="userRepository"/>
</bean>
4. Using @Bean Annotation in Java Configuration
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean(initMethod = "initMethod")
public UserService userService(UserRepository userRepository) {
return new UserService(userRepository);
}
}
Bean Post-Processing
Spring's BeanPostProcessor
interfaces allow you to intercept and modify beans before and after initialization.
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class CustomBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
System.out.println("BeanPostProcessor - Before Initialization for: " + beanName);
return bean; // You can return a different object to replace the original bean
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
System.out.println("BeanPostProcessor - After Initialization for: " + beanName);
return bean; // You can return a modified or wrapped bean
}
}
To register this post-processor, you can define it as a bean:
@Configuration
public class AppConfig {
@Bean
public CustomBeanPostProcessor customBeanPostProcessor() {
return new CustomBeanPostProcessor();
}
}
Bean Destruction Callbacks
When the Spring container is shut down, beans can perform cleanup operations:
1. Implementing DisposableBean Interface
import org.springframework.beans.factory.DisposableBean;
public class DatabaseConnectionManager implements DisposableBean {
private Connection connection;
public void openConnection() {
System.out.println("Opening database connection");
// Logic to open connection
}
@Override
public void destroy() throws Exception {
System.out.println("DisposableBean's destroy method called");
// Cleanup logic - close connections, release resources
if (connection != null) {
connection.close();
System.out.println("Database connection closed");
}
}
}
2. Using @PreDestroy Annotation
import javax.annotation.PreDestroy;
public class CacheManager {
private Map<String, Object> cache = new HashMap<>();
public void storeInCache(String key, Object value) {
cache.put(key, value);
}
@PreDestroy
public void clearCache() {
System.out.println("@PreDestroy method called - clearing cache");
cache.clear();
}
}
3. Using destroy-method Attribute in XML or Java Configuration
public class FileHandler {
private FileWriter writer;
public void openFile(String filePath) throws IOException {
writer = new FileWriter(filePath);
}
public void writeToFile(String content) throws IOException {
writer.write(content);
}
public void closeFile() throws IOException {
System.out.println("Custom destroy method called - closing file");
if (writer != null) {
writer.close();
}
}
}
XML configuration:
<bean id="fileHandler" class="com.example.FileHandler" destroy-method="closeFile">
<!-- properties -->
</bean>
Java configuration:
@Configuration
public class AppConfig {
@Bean(destroyMethod = "closeFile")
public FileHandler fileHandler() {
return new FileHandler();
}
}
Complete Bean Lifecycle Example
Let's create a comprehensive example that demonstrates the entire lifecycle of a Spring bean:
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
public class LifecycleDemoBean implements InitializingBean, DisposableBean {
public LifecycleDemoBean() {
System.out.println("1. Constructor called");
}
public void setProperty(String property) {
System.out.println("2. Setter method called with value: " + property);
}
@PostConstruct
public void postConstructMethod() {
System.out.println("3. @PostConstruct method called");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("4. InitializingBean's afterPropertiesSet method called");
}
public void customInitMethod() {
System.out.println("5. Custom init method called");
}
// Bean is now ready for use
public void businessMethod() {
System.out.println("6. Business method execution");
}
@PreDestroy
public void preDestroyMethod() {
System.out.println("7. @PreDestroy method called");
}
@Override
public void destroy() throws Exception {
System.out.println("8. DisposableBean's destroy method called");
}
public void customDestroyMethod() {
System.out.println("9. Custom destroy method called");
}
}
Java configuration to set up this bean:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean(initMethod = "customInitMethod", destroyMethod = "customDestroyMethod")
public LifecycleDemoBean lifecycleDemoBean() {
LifecycleDemoBean bean = new LifecycleDemoBean();
bean.setProperty("sample property");
return bean;
}
}
Main application to demonstrate the lifecycle:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class SpringBeanLifecycleDemo {
public static void main(String[] args) {
// Create and configure the Spring container
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
// Get the bean
LifecycleDemoBean bean = context.getBean(LifecycleDemoBean.class);
// Use the bean
bean.businessMethod();
// Close the context to trigger destroy methods
context.close();
}
}
Output:
1. Constructor called
2. Setter method called with value: sample property
3. @PostConstruct method called
4. InitializingBean's afterPropertiesSet method called
5. Custom init method called
6. Business method execution
7. @PreDestroy method called
8. DisposableBean's destroy method called
9. Custom destroy method called
Practical Use Cases for Bean Lifecycle Methods
1. Resource Management
Initialize connections or resources during initialization and close them during destruction:
public class DatabaseService {
private Connection connection;
@PostConstruct
public void initializeConnection() throws SQLException {
System.out.println("Opening database connection");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb",
"username", "password");
}
public void executeQuery(String sql) throws SQLException {
try (Statement stmt = connection.createStatement()) {
return stmt.executeQuery(sql);
}
}
@PreDestroy
public void closeConnection() throws SQLException {
System.out.println("Closing database connection");
if (connection != null && !connection.isClosed()) {
connection.close();
}
}
}
2. Cache Initialization
Load frequently used data into a cache during initialization:
public class ProductCacheService {
private Map<Long, Product> productCache = new HashMap<>();
private final ProductRepository productRepository;
public ProductCacheService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@PostConstruct
public void loadProductCache() {
System.out.println("Initializing product cache");
List<Product> featuredProducts = productRepository.findFeaturedProducts();
for (Product product : featuredProducts) {
productCache.put(product.getId(), product);
}
System.out.println("Loaded " + productCache.size() + " products into cache");
}
public Product getProduct(Long id) {
return productCache.getOrDefault(id, productRepository.findById(id));
}
@PreDestroy
public void clearCache() {
System.out.println("Clearing product cache");
productCache.clear();
}
}
3. Validation During Initialization
Validate that all required dependencies are properly set:
public class EmailService {
private SMTPServer smtpServer;
private String fromAddress;
private TemplateEngine templateEngine;
public EmailService(SMTPServer smtpServer, TemplateEngine templateEngine) {
this.smtpServer = smtpServer;
this.templateEngine = templateEngine;
}
public void setFromAddress(String fromAddress) {
this.fromAddress = fromAddress;
}
@PostConstruct
public void validateConfiguration() {
if (smtpServer == null) {
throw new IllegalStateException("SMTP server must be configured");
}
if (fromAddress == null || fromAddress.isEmpty()) {
throw new IllegalStateException("From address must be set");
}
if (templateEngine == null) {
throw new IllegalStateException("Template engine must be configured");
}
System.out.println("EmailService properly configured");
}
public void sendEmail(String to, String subject, String templateName,
Map<String, Object> templateData) {
// Email sending logic
}
}
Best Practices for Spring Bean Lifecycle Management
-
Prefer annotations over XML configuration: Use
@PostConstruct
and@PreDestroy
annotations instead of XMLinit-method
anddestroy-method
attributes when possible. -
Use the most specific approach: Use the Spring interfaces (
InitializingBean
andDisposableBean
) only when needed, as they couple your code to Spring. -
Keep initialization methods fast: Initialization methods should execute quickly to avoid slowing down application startup.
-
Clean up resources: Always release resources in destruction callbacks to prevent memory leaks.
-
Handle exceptions properly: Initialization methods should handle exceptions gracefully or propagate them clearly.
-
Don't rely on destruction callbacks for critical tasks: The JVM might shut down without calling destruction callbacks, so don't rely on them for critical operations.
-
Be aware of bean scope: Destruction callbacks are only called for singleton beans by default. For prototype-scoped beans, you need to handle resource cleanup yourself.
Summary
The Spring Bean Lifecycle provides powerful hooks for initializing and cleaning up resources. Understanding these lifecycle phases allows you to properly manage resources, validate configuration, and ensure your application works correctly.
Key concepts we've covered:
- Bean initialization phases (instantiation, dependency injection, initialization)
- Various initialization callbacks (
@PostConstruct
,InitializingBean
, custom init methods) - Bean post-processing
- Destruction callbacks (
@PreDestroy
,DisposableBean
, custom destroy methods) - Practical use cases for lifecycle methods
- Best practices for managing bean lifecycles
By properly utilizing these lifecycle hooks, you can create more robust and resource-efficient Spring applications.
Additional Resources
- Spring Framework Documentation - Bean Lifecycle
- Spring Framework Reference - @PostConstruct and @PreDestroy
- Spring Framework API - BeanPostProcessor
Exercises
-
Create a
FileService
bean that opens a file during initialization and closes it during destruction. -
Implement a custom
BeanPostProcessor
that logs the creation of all beans in your application. -
Create a bean with configuration validation in a
@PostConstruct
method and test what happens when validation fails. -
Implement a
CacheManager
bean that loads data from a database during initialization and correctly handles cleanup. -
Compare the performance difference between placing initialization code in the constructor versus a
@PostConstruct
method for a complex bean.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)