Spring Bean Lifecycle: Destruction
Bean Destruction Lifecycle
When the Spring container shuts down, it invokes the lifecycle methods for Bean destruction, as shown in the diagram below.
Case Study: Closing Database Connections on Bean Destruction
Consider a scenario where a database connection pool is managed as a Bean. Using the @PreDestroy annotation, we define a shutdown() method within the DatabaseConnectionPool class. This method is called when the Spring container is destroyed, ensuring that connections are properly closed before the application terminates. Here is the code:
@Component
public class DatabaseConnectionPool {
private final String url = "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1";
private final String username = "sa";
private final String password = "";
private final int initialPoolSize = 5;
private Queue<Connection> connectionPool;
private List<Connection> activeConnections;
public DatabaseConnectionPool() {
this.connectionPool = new ConcurrentLinkedQueue<>();
this.activeConnections = new ArrayList<>();
}
@PostConstruct
public void init() {
// Initialization code
}
@PreDestroy
public void shutdown() {
for (Connection conn : activeConnections) {
try {
if (conn != null && !conn.isClosed()) {
conn.close();
System.out.println("DatabaseConnectionPool: Closed connection: " + conn);
}
} catch (SQLException e) {
System.err.println("DatabaseConnectionPool: Failed to close connection: " + e.getMessage());
}
}
connectionPool.clear();
activeConnections.clear();
}
}
Bean Destruction Source Code Analysis
Registering DisposableBean
During Bean creation, in the AbstractAutowireCapableBeanFactory's doCreateBean() method, it checks if a Bean should be destroyed when the container closes. If so, it registers a DisposableBeanAdapter object for the current Bean. Here is the code:
protected void registerDisposableBeanIfNecessary(String beanName, Object bean, RootBeanDefinition mbd) {
if (!mbd.isPrototype() && requiresDestruction(bean, mbd)) {
if (mbd.isSingleton()) {
registerDisposableBean(beanName, new DisposableBeanAdapter(
bean, beanName, mbd, getBeanPostProcessorCache().destructionAware));
} else {
Scope scope = this.scopes.get(mbd.getScope());
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name '" + mbd.getScope() + "'");
}
scope.registerDestructionCallback(beanName, new DisposableBeanAdapter(
bean, beanName, mbd, getBeanPostProcessorCache().destructionAware));
}
}
}
The requiresDestruction() method determines if a Bean needs destruction by checking if it implements the DisposableBean interface or has defined a destroy method. This is achieved by examining the destroyMethodNames property, which holds the method names configured via @Bean's destroyMethod attribute or XML's destroy-method. Here is the code:
protected boolean requiresDestruction(Object bean, RootBeanDefinition mbd) {
return (bean.getClass() != NullBean.class && (DisposableBeanAdapter.hasDestroyMethod(bean, mbd) ||
(hasDestructionAwareBeanPostProcessors() && DisposableBeanAdapter.hasApplicableProcessors(
bean, getBeanPostProcessorCache().destructionAware))));
}
public static boolean hasDestroyMethod(Object bean, RootBeanDefinition beanDefinition) {
return (bean instanceof DisposableBean ||
inferDestroyMethodsIfNecessary(bean.getClass(), beanDefinition) != null);
}
static String[] inferDestroyMethodsIfNecessary(Class<?> target, RootBeanDefinition beanDefinition) {
String[] destroyMethodNames = beanDefinition.getDestroyMethodNames();
if (destroyMethodNames != null && destroyMethodNames.length > 1) {
return destroyMethodNames;
}
// Additional code omitted
}
Next, it checks for DestructionAwareBeanPostProcessor, invoking its requiresDestruction() method. The @PreDestroy annotated method is processed by InitDestroyAnnotationBeanPostProcessor, whose parent class CommonAnnotationBeanPostProcessor initializes handling of @PreDestroy annotations in its constructor. Here is the code:
class DisposableBeanAdapter {
public static boolean hasApplicableProcessors(Object bean, List<DestructionAwareBeanPostProcessor> postProcessors) {
if (!CollectionUtils.isEmpty(postProcessors)) {
for (DestructionAwareBeanPostProcessor processor : postProcessors) {
if (processor.requiresDestruction(bean)) {
return true;
}
}
}
return false;
}
}
public class InitDestroyAnnotationBeanPostProcessor {
public boolean requiresDestruction(Object bean) {
return findLifecycleMetadata(bean.getClass()).hasDestroyMethods();
}
private LifecycleMetadata findLifecycleMetadata(Class<?> beanClass) {
if (this.lifecycleMetadataCache == null) {
return buildLifecycleMetadata(beanClass);
}
LifecycleMetadata metadata = this.lifecycleMetadataCache.get(beanClass);
if (metadata == null) {
synchronized (this.lifecycleMetadataCache) {
metadata = this.lifecycleMetadataCache.get(beanClass);
if (metadata == null) {
metadata = buildLifecycleMetadata(beanClass);
this.lifecycleMetadataCache.put(beanClass, metadata);
}
return metadata;
}
}
return metadata;
}
}
public class CommonAnnotationBeanPostProcessor {
public CommonAnnotationBeanPostProcessor() {
setOrder(Ordered.LOWEST_PRECEDENCE - 3);
addInitAnnotationType(loadAnnotationType("jakarta.annotation.PostConstruct"));
addDestroyAnnotationType(loadAnnotationType("jakarta.annotation.PreDestroy"));
addInitAnnotationType(loadAnnotationType("javax.annotation.PostConstruct"));
addDestroyAnnotationType(loadAnnotationType("javax.annotation.PreDestroy"));
if (jndiPresent) {
this.jndiFactory = new SimpleJndiBeanFactory();
}
}
}
JVM Shutdown Hook Registration
The AbstractApplicationContext provides a registerShutdownHook() method, allowing registration of a hook thread with the JVM. This thread runs upon JVM termination to perform container destruction operations. Developers can manually call this method in their business code. Here is the code:
public class AbstractApplicationContext {
public void registerShutdownHook() {
if (this.shutdownHook == null) {
this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) {
@Override
public void run() {
if (isStartupShutdownThreadStuck()) {
active.set(false);
return;
}
startupShutdownLock.lock();
try {
doClose();
} finally {
startupShutdownLock.unlock();
}
}
};
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
}
}
public class SpringTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
context.registerShutdownHook();
}
}
In Spring Boot projects, SpringApplicationShutdownHook handles registration based on the spring.main.register-shutdown-hook property. Here is the code:
public class SpringApplication {
public ConfigurableApplicationContext run(String... args) {
Startup startup = Startup.create();
if (this.properties.isRegisterShutdownHook()) {
SpringApplication.shutdownHook.enableShutdownHookAddition();
}
}
private void refreshContext(ConfigurableApplicationContext context) {
if (this.properties.isRegisterShutdownHook()) {
shutdownHook.registerApplicationContext(context);
}
refresh(context);
}
}
class SpringApplicationShutdownHook implements Runnable {
void registerApplicationContext(ConfigurableApplicationContext context) {
addRuntimeShutdownHookIfNecessary();
synchronized (SpringApplicationShutdownHook.class) {
assertNotInProgress();
context.addApplicationListener(this.contextCloseListener);
this.contexts.add(context);
}
}
private void addRuntimeShutdownHookIfNecessary() {
if (this.shutdownHookAdditionEnabled && this.shutdownHookAdded.compareAndSet(false, true)) {
addRuntimeShutdownHook();
}
}
void addRuntimeShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread(this, "SpringApplicationShutdownHook"));
}
@Override
public void run() {
Set<ConfigurableApplicationContext> contexts;
Set<ConfigurableApplicationContext> closedContexts;
List<Handler> handlers;
synchronized (SpringApplicationShutdownHook.class) {
this.inProgress = true;
contexts = new LinkedHashSet<>(this.contexts);
closedContexts = new LinkedHashSet<>(this.closedContexts);
handlers = new ArrayList<>(this.handlers.getActions());
Collections.reverse(handlers);
}
contexts.forEach(this::closeAndWait);
closedContexts.forEach(this::closeAndWait);
handlers.forEach(Handler::run);
}
}
Invocation of Bean Destruction Methods
When the registered callback thread starts, it calls the AbstractApplicationContext's doClose() method, which then calls destroyBeans(). This method ultimately calls DefaultSingletonBeanRegistry's destroySingletons() method. Here is the code:
public abstract class AbstractApplicationContext {
protected void doClose() {
if (this.active.get() && this.closed.compareAndSet(false, true)) {
destroyBeans();
}
}
protected void destroyBeans() {
getBeanFactory().destroySingletons();
}
}
class DefaultSingletonBeanRegistry {
public void destroySingletons() {
this.singletonsCurrentlyInDestruction = true;
String[] disposableBeanNames;
synchronized (this.disposableBeans) {
disposableBeanNames = StringUtils.toStringArray(this.disposableBeans.keySet());
}
for (int i = disposableBeanNames.length - 1; i >= 0; i--) {
destroySingleton(disposableBeanNames[i]);
}
this.containedBeanMap.clear();
this.dependentBeanMap.clear();
this.dependenciesForBeanMap.clear();
this.singletonLock.lock();
try {
clearSingletonCache();
} finally {
this.singletonLock.unlock();
}
}
}
In DefaultSingletonBeanRegistry's destroySingletons() method, it iteratively calls the previously registered DisposableBeanAdapter's destroy() method, which handles the invocation of lifecycle methods. It first calls DestructionAwareBeanPostProcessor's postProcessBeforeDestruction() method to handle @PreDestroy annotated methods, then calls DisposableBean's destroy() method, and finally custom destroy methods. Here is the code:
public void destroy() {
if (!CollectionUtils.isEmpty(this.beanPostProcessors)) {
for (DestructionAwareBeanPostProcessor processor : this.beanPostProcessors) {
processor.postProcessBeforeDestruction(this.bean, this.beanName);
}
}
if (this.invokeDisposableBean) {
try {
((DisposableBean) this.bean).destroy();
} catch (Throwable ex) {
// Exception handling
}
} else if (this.destroyMethods != null) {
for (Method destroyMethod : this.destroyMethods) {
invokeCustomDestroyMethod(destroyMethod);
}
}
}
This comprehensive overview covers the entire process of Bean destruction in a Spring container, including lifecycle methods and how they are invoked.