Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Spring Bean Lifecycle: Destruction

Tech 3

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.

Tags: spring

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.