Spring Bean Lifecycle: @PostConstruct, @PreDestroy and More
Introduction
This tutorial follows a Spring bean through its lifecycle - from the moment the container creates it, through the callbacks that prepare it for use, to the callbacks that clean it up when the container shuts down. We will start with an overview of the lifecycle phases. We will then use the most common callbacks,@PostConstruct and @PreDestroy, and see why initialization belongs there rather than in the constructor. Next we will put every kind of callback into one bean - the Aware interfaces, InitializingBean and DisposableBean, and the initMethod and destroyMethod attributes of @Bean - and print the exact order in which Spring calls them. We will then write a BeanPostProcessor, the hook that lets Spring add behaviour to every bean, see how a destroy method can be inferred automatically, and lastly see why prototype beans are never destroyed by the container.(This tutorial assumes you know singleton and prototype scopesClick to read tutorial: Spring Bean Scopes: Singleton, Prototype, Request, Session; the lifecycle differs between them.)
The phases of a bean's life
For a singleton bean, the container goes through these phases, in this order -- Instantiation: the container calls the constructor (with constructor injection, the dependencies arrive here).
- Populating properties: setter and field injection.
- Aware callbacks: if the bean implements interfaces such as
BeanNameAwareorApplicationContextAware, the container hands it its own name or the context. BeanPostProcessorbefore initialization: every registered post-processor sees the bean.- Initialization:
@PostConstructmethods, thenInitializingBean.afterPropertiesSet(), then a custominitMethod. BeanPostProcessorafter initialization: post-processors can now replace the bean, for example with a proxy that adds transactions.- In use: the bean is injected into others and serves calls until the context closes.
- Destruction, when the context closes:
@PreDestroymethods, thenDisposableBean.destroy(), then a customdestroyMethod.
@PostConstruct method and a @PreDestroy method. The rest is used mainly by frameworks, including Spring itself.
@PostConstruct and @PreDestroy
@PostConstruct marks a method Spring calls once the bean is fully created and all its dependencies are injected. @PreDestroy marks a method Spring calls just before it discards the bean. Both annotations come from the Jakarta Annotations API (package jakarta.annotation), which Spring Boot starters include. The example is a connection pool that opens connections at startup and closes them at shutdown -
Opening resources in @PostConstruct and releasing them in @PreDestroy
//PostConstructApp.java
package com.javabrahman.spring.lifecycle.basic;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
@Component
class PoolSettings {
int size() {
return 3;
}
}
@Component
class ConnectionPool {
private final List<String> connections = new ArrayList<>();
private PoolSettings settings;
ConnectionPool() {
System.out.println("1. Constructor - settings injected yet? " + (settings != null));
}
@Autowired
void setSettings(PoolSettings settings) {
this.settings = settings;
System.out.println("2. Setter injection - settings injected");
}
@PostConstruct
void open() {
for (int i = 1; i <= settings.size(); i++) {
connections.add("connection-" + i);
}
System.out.println("3. @PostConstruct - opened " + connections);
}
String borrow() {
return connections.get(0);
}
@PreDestroy
void close() {
System.out.println("5. @PreDestroy - closing " + connections.size() + " connections");
connections.clear();
}
}
@Configuration
@ComponentScan
class AppConfig {
}
public class PostConstructApp {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
System.out.println("4. In use - borrowed " + context.getBean(ConnectionPool.class).borrow());
System.out.println(" Closing the context");
}
System.out.println("Context closed");
}
}
1. Constructor - settings injected yet? false 2. Setter injection - settings injected 3. @PostConstruct - opened [connection-1, connection-2, connection-3] 4. In use - borrowed connection-1 Closing the context 5. @PreDestroy - closing 3 connections Context closed
Explanation of the code
- In the constructor, the setter-injected
settingswas stillnull. Code that needs injected dependencies cannot run there - and with field or setter injection that is the usual case. @PostConstructran after injection, so it could readsettings.size()and open three connections. It runs once, before any other bean can use the pool.- Closing the context - here at the end of the try-with-resources block - called
@PreDestroy, which released the connections. - The annotated methods can have any name and any access level, take no parameters, and return
void. - With constructor injection, the dependencies are already available in the constructor, but
@PostConstructis still the better place for work such as opening connections, starting threads or loading a cache: the constructor stays cheap, and the object is fully built (including any subclass that Spring generates) before the work starts.
Every callback in one bean, in order
Spring has three equivalent ways to declare an initialization method and three to declare a destroy method, plus the Aware interfaces. The bean below uses all of them so that the output shows the order in which Spring calls them -All lifecycle callbacks on one bean, printed in the order Spring calls them
//CallbackOrderApp.java
package com.javabrahman.spring.lifecycle.order;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
class ReportCache implements BeanNameAware, ApplicationContextAware, InitializingBean, DisposableBean {
private int step = 0;
ReportCache() {
log("constructor");
}
@Override
public void setBeanName(String name) {
log("BeanNameAware.setBeanName(\"" + name + "\")");
}
@Override
public void setApplicationContext(ApplicationContext context) {
log("ApplicationContextAware.setApplicationContext()");
}
@PostConstruct
void postConstruct() {
log("@PostConstruct");
}
@Override
public void afterPropertiesSet() {
log("InitializingBean.afterPropertiesSet()");
}
void customInit() {
log("@Bean(initMethod) customInit()");
}
@PreDestroy
void preDestroy() {
log("@PreDestroy");
}
@Override
public void destroy() {
log("DisposableBean.destroy()");
}
void customDestroy() {
log("@Bean(destroyMethod) customDestroy()");
}
private void log(String callback) {
System.out.println(++step + ". " + callback);
}
}
@Configuration
class AppConfig {
@Bean(initMethod = "customInit", destroyMethod = "customDestroy")
ReportCache reportCache() {
return new ReportCache();
}
}
public class CallbackOrderApp {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
System.out.println("-- context started, closing it --");
}
}
}
1. constructor
2. BeanNameAware.setBeanName("reportCache")
3. ApplicationContextAware.setApplicationContext()
4. @PostConstruct
5. InitializingBean.afterPropertiesSet()
6. @Bean(initMethod) customInit()
-- context started, closing it --
7. @PreDestroy
8. DisposableBean.destroy()
9. @Bean(destroyMethod) customDestroy()
Explanation of the code
- After the constructor, the Aware callbacks gave the bean its name (
reportCache, the@Beanmethod name) and the application context. - Initialization ran in a fixed order:
@PostConstruct, thenafterPropertiesSet(), then theinitMethod. - Destruction mirrored it:
@PreDestroy, thendestroy(), then thedestroyMethod. - Use one mechanism per bean, not three. For your own classes, use
@PostConstructand@PreDestroy: they are standard Java annotations and keep Spring interfaces out of the class.InitializingBeanandDisposableBeantie the class to Spring; they are mostly found inside Spring and other frameworks.initMethodanddestroyMethodare for library classes you cannot annotate, such as a connection pool from a third-party jar. - The Aware interfaces are rarely needed in application code. Instead of
ApplicationContextAware, inject what you need - includingApplicationContextitself, which can be injected like any bean.
BeanPostProcessor: a hook into every bean
ABeanPostProcessor is a special bean that the container calls for every other bean, once before and once after its initialization callbacks. It can inspect a bean, change it, or return a different object in its place. This is how Spring implements much of its own magic: @PostConstruct and @Autowired are processed by built-in post-processors, and @Transactional, @Async and @Cacheable work because a post-processor replaces the bean with a proxy after initialization. The example writes a post-processor that times how long each bean takes to initialize -
A BeanPostProcessor that measures each bean's initialization
//PostProcessorApp.java
package com.javabrahman.spring.lifecycle.postprocessor;
import jakarta.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
class InitTimer implements BeanPostProcessor {
private final Map<String, Long> startTimes = new HashMap<>();
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
if (bean.getClass().getPackageName().startsWith("com.javabrahman")) {
startTimes.put(beanName, System.nanoTime());
System.out.println("before init: " + beanName);
}
return bean; // return the bean itself, or a replacement
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
Long start = startTimes.remove(beanName);
if (start != null) {
long millis = (System.nanoTime() - start) / 1_000_000;
System.out.println("after init: " + beanName + (millis >= 100 ? " - slow, took at least 100 ms" : " - fast"));
}
return bean;
}
}
@Component
class SpellChecker {
@PostConstruct
void loadDictionary() throws InterruptedException {
System.out.println(" SpellChecker @PostConstruct: loading the dictionary");
Thread.sleep(150); // stands in for slow start-up work
}
}
@Component
class WordCounter {
@PostConstruct
void init() {
System.out.println(" WordCounter @PostConstruct");
}
}
@Configuration
@ComponentScan
class AppConfig {
// static: post-processors must exist before the other beans are created
@Bean
static InitTimer initTimer() {
return new InitTimer();
}
}
public class PostProcessorApp {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
System.out.println("Context ready");
}
}
}
before init: appConfig after init: appConfig - fast before init: spellChecker SpellChecker @PostConstruct: loading the dictionary after init: spellChecker - slow, took at least 100 ms before init: wordCounter WordCounter @PostConstruct after init: wordCounter - fast Context ready
Explanation of the code
- The container detected
InitTimeras aBeanPostProcessorand called it for every bean it created afterwards; the example limits the output to our own beans by package. - For each bean,
postProcessBeforeInitialization()ran first, then the bean's@PostConstructmethod, thenpostProcessAfterInitialization()- so the timer measured exactly the initialization phase and caught the slowSpellChecker. (The output says "at least 100 ms" rather than printing the exact time, which varies from run to run.) - The
@Beanmethod isstatic. Post-processors are created very early, before ordinary beans, and a static method lets Spring call it without first creatingAppConfigand processing it. - A post-processor that returned a different object from
postProcessAfterInitialization()- for example a proxy that logs every method call - would replace the bean everywhere it is injected. That is the mechanism behind Spring AOP, covered in module 7 of this series.
Destroy methods inferred for @Bean
For beans declared with@Bean, you often do not need to name a destroy method at all. By default, Spring infers one: if the returned object has a public close() or shutdown() method - which includes every AutoCloseable - Spring calls it when the context closes -
Spring calls close() on an AutoCloseable @Bean automatically
//InferredDestroyApp.java
package com.javabrahman.spring.lifecycle.inferred;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// Stands in for a third-party client class with no Spring annotations
class HttpClientWrapper implements AutoCloseable {
HttpClientWrapper() {
System.out.println("HttpClientWrapper opened");
}
@Override
public void close() {
System.out.println("HttpClientWrapper.close() called by Spring");
}
}
class KeepOpenClient implements AutoCloseable {
@Override
public void close() {
System.out.println("This line never prints");
}
}
@Configuration
class AppConfig {
@Bean // destroyMethod is inferred: close()
HttpClientWrapper httpClient() {
return new HttpClientWrapper();
}
@Bean(destroyMethod = "") // an empty name switches inference off
KeepOpenClient sharedClient() {
return new KeepOpenClient();
}
}
public class InferredDestroyApp {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
System.out.println("Closing the context");
}
}
}
HttpClientWrapper opened Closing the context HttpClientWrapper.close() called by Spring
Explanation of the code
HttpClientWrapperhas no annotations and the@Beannames no destroy method, yet Spring called itsclose()at shutdown.destroyMethod = ""switched inference off forsharedClient, for an object whose lifetime something else controls, so itsclose()was not called.- Inference applies to
@Beanmethods. A@Componentclass gets the same behaviour if it implementsAutoCloseableorDisposableBean, or has a@PreDestroymethod.
Prototype beans are not destroyed
The container initializes a prototype bean like any other - its@PostConstruct runs - but then hands it over and keeps no reference to it. So when the context closes, it has nothing to destroy -
Initialization callbacks run for prototypes, destroy callbacks do not
//PrototypeLifecycleApp.java
package com.javabrahman.spring.lifecycle.prototype;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("prototype")
class ExportJob {
private static int instances = 0;
private final int number = ++instances;
@PostConstruct
void init() {
System.out.println("ExportJob #" + number + " @PostConstruct");
}
@PreDestroy
void cleanUp() {
System.out.println("ExportJob #" + number + " @PreDestroy");
}
}
@Component
class Settings {
@PreDestroy
void cleanUp() {
System.out.println("Settings (singleton) @PreDestroy");
}
}
@Configuration
@ComponentScan
class AppConfig {
}
public class PrototypeLifecycleApp {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
context.getBean(ExportJob.class);
context.getBean(ExportJob.class);
System.out.println("Closing the context");
}
}
}
ExportJob #1 @PostConstruct ExportJob #2 @PostConstruct Closing the context Settings (singleton) @PreDestroy
Explanation of the code
- Both
ExportJobinstances ran@PostConstructwhen they were created. - On close, only the singleton
Settingswas destroyed. NeitherExportJobran@PreDestroy, because the container no longer knew about them. - If a prototype holds resources - a file, a connection, a thread - the code that requested it must release them, for example by making the prototype
AutoCloseableand using it in a try-with-resources block. Memory is not a problem: once the caller drops its references, the garbage collectorClick to read: garbage collector reclaims the object as usual.
Lifecycle best practices
- Keep constructors cheap: assign dependencies and validate arguments. Do start-up work - opening connections, loading data, starting threads - in
@PostConstruct. - Release everything you acquired in
@PreDestroy(orclose()), and make sure the context is actually closed: use try-with-resources orregisterShutdownHook()in plain Spring; Spring Boot does it for you. - Prefer
@PostConstructand@PreDestroyfor your own classes, andinitMethod/destroyMethod(or inference) for library classes. - Keep
@PostConstructmethods fast and free of calls through the bean's own proxy. A slow initializer delays application start-up; for long jobs, start them in the background or listen for the context-ready event instead. - Write a
BeanPostProcessoronly for cross-cutting framework-style behaviour. For behaviour around method calls, Spring AOP is the higher-level tool.
Summary
In this tutorial we first went through the phases of a bean's life, from instantiation to destruction. We used@PostConstruct to open resources once the dependencies were injected, and @PreDestroy to release them when the context closed. We then put every callback - the Aware interfaces, InitializingBean and DisposableBean, and initMethod and destroyMethod - into one bean and printed the order Spring calls them in. Next we wrote a BeanPostProcessor that timed every bean's initialization, and saw that Spring infers close() as the destroy method of @Bean objects. Lastly, we saw that prototype beans are initialized but never destroyed by the container, and listed best practices. The next tutorial deals with choosing between several beans of the same type: @Qualifier, @Primary and conditional beansClick to read tutorial: Spring @Qualifier, @Primary and Conditional Beans.Spring Tutorials
Spring Bean Scopes: Singleton, Prototype, Request, SessionClick to read tutorial: Spring Bean Scopes: Singleton, Prototype, Request, Session Spring @Qualifier, @Primary and Conditional BeansClick to read tutorial: Spring @Qualifier, @Primary and Conditional Beans What is Spring? Spring Framework vs Spring Boot ExplainedClick to read tutorial: What is Spring? Spring Framework vs Spring Boot Explained Spring IoC Container and ApplicationContext ExplainedClick to read tutorial: Spring IoC Container and ApplicationContext Explained Spring Beans: @Component, @Bean and @ConfigurationClick to read tutorial: Spring Beans: @Component, @Bean and @Configuration Spring Dependency Injection: Constructor vs Setter vs FieldClick to read tutorial: Spring Dependency Injection: Constructor vs Setter vs Field Spring Expression Language (SpEL) and @Value ExplainedClick to read tutorial: Spring Expression Language (SpEL) and @Value Explained
Spring Bean Scopes: Singleton, Prototype, Request, SessionClick to read tutorial: Spring Bean Scopes: Singleton, Prototype, Request, Session Spring @Qualifier, @Primary and Conditional BeansClick to read tutorial: Spring @Qualifier, @Primary and Conditional Beans What is Spring? Spring Framework vs Spring Boot ExplainedClick to read tutorial: What is Spring? Spring Framework vs Spring Boot Explained Spring IoC Container and ApplicationContext ExplainedClick to read tutorial: Spring IoC Container and ApplicationContext Explained Spring Beans: @Component, @Bean and @ConfigurationClick to read tutorial: Spring Beans: @Component, @Bean and @Configuration Spring Dependency Injection: Constructor vs Setter vs FieldClick to read tutorial: Spring Dependency Injection: Constructor vs Setter vs Field Spring Expression Language (SpEL) and @Value ExplainedClick to read tutorial: Spring Expression Language (SpEL) and @Value Explained