Skip to main content

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:

  1. Bean Instantiation - Spring creates an instance of the bean
  2. Dependency Injection - Properties and dependencies are set
  3. Bean Post-Processing - Pre-Initialization - BeanPostProcessors can modify beans before initialization
  4. Bean Initialization - Custom initialization methods are called
  5. Bean Post-Processing - Post-Initialization - BeanPostProcessors can modify beans after initialization
  6. Bean is Ready for Use - The bean is used by the application
  7. 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.

java
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

java
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

java
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

java
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:

xml
<bean id="userService" class="com.example.UserService" init-method="initMethod">
<constructor-arg ref="userRepository"/>
</bean>

4. Using @Bean Annotation in Java Configuration

java
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.

java
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:

java
@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

java
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

java
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

java
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:

xml
<bean id="fileHandler" class="com.example.FileHandler" destroy-method="closeFile">
<!-- properties -->
</bean>

Java configuration:

java
@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:

java
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:

java
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:

java
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:

java
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:

java
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:

java
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

  1. Prefer annotations over XML configuration: Use @PostConstruct and @PreDestroy annotations instead of XML init-method and destroy-method attributes when possible.

  2. Use the most specific approach: Use the Spring interfaces (InitializingBean and DisposableBean) only when needed, as they couple your code to Spring.

  3. Keep initialization methods fast: Initialization methods should execute quickly to avoid slowing down application startup.

  4. Clean up resources: Always release resources in destruction callbacks to prevent memory leaks.

  5. Handle exceptions properly: Initialization methods should handle exceptions gracefully or propagate them clearly.

  6. 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.

  7. 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

Exercises

  1. Create a FileService bean that opens a file during initialization and closes it during destruction.

  2. Implement a custom BeanPostProcessor that logs the creation of all beans in your application.

  3. Create a bean with configuration validation in a @PostConstruct method and test what happens when validation fails.

  4. Implement a CacheManager bean that loads data from a database during initialization and correctly handles cleanup.

  5. 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! :)