This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch WW-5537-classloader-leak-fixes
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 4aa8a0a3e1e79273b22a371061b784ac6928126f
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon Mar 23 10:01:40 2026 +0100

    WW-5537 Dispatcher.cleanup: refactor into focused methods with 
InternalDestroyable discovery
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../org/apache/struts2/dispatcher/Dispatcher.java  | 100 +++++++++++++++++----
 1 file changed, 81 insertions(+), 19 deletions(-)

diff --git a/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java 
b/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java
index 5cbd66b56..0bf74917e 100644
--- a/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java
+++ b/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java
@@ -441,7 +441,36 @@ public class Dispatcher {
      * Releases all instances bound to this dispatcher instance.
      */
     public void cleanup() {
-        // clean up ObjectFactory
+        destroyObjectFactory();
+
+        // clean up Dispatcher itself for this thread
+        instance.remove();
+        servletContext.setAttribute(StrutsStatics.SERVLET_DISPATCHER, null);
+
+        destroyDispatcherListeners();
+
+        destroyInterceptors();
+
+        destroyInternalBeans();
+
+        // WW-5537: Invalidate all threads' cached Container references to 
prevent
+        // classloader leaks from idle pool threads retaining stale references 
after undeploy.
+        ContainerHolder.invalidateAll();
+
+        //cleanup action context
+        ActionContext.clear();
+
+        // clean up configuration
+        configurationManager.destroyConfiguration();
+        configurationManager = null;
+    }
+
+    /**
+     * Destroys the {@link ObjectFactory} if it implements {@link 
ObjectFactoryDestroyable}.
+     *
+     * @since 7.1.0
+     */
+    protected void destroyObjectFactory() {
         if (objectFactory == null) {
             LOG.warn("Object Factory is null, something is seriously wrong, no 
clean up will be performed");
         }
@@ -449,29 +478,40 @@ public class Dispatcher {
             try {
                 ((ObjectFactoryDestroyable) objectFactory).destroy();
             } catch (Exception e) {
-                // catch any exception that may occur during destroy() and log 
it
                 LOG.error("Exception occurred while destroying ObjectFactory 
[{}]", objectFactory.toString(), e);
             }
         }
+    }
 
-        // clean up Dispatcher itself for this thread
-        instance.remove();
-        servletContext.setAttribute(StrutsStatics.SERVLET_DISPATCHER, null);
-
-        // clean up DispatcherListeners
+    /**
+     * Notifies all registered {@link DispatcherListener}s that this dispatcher
+     * is being destroyed, then clears the listener list.
+     *
+     * @since 7.1.0
+     */
+    protected void destroyDispatcherListeners() {
         if (!dispatcherListeners.isEmpty()) {
             for (DispatcherListener l : dispatcherListeners) {
                 l.dispatcherDestroyed(this);
             }
+            // WW-5537: Clear the static listener list to release references 
that may
+            // pin the webapp classloader after undeploy.
+            dispatcherListeners.clear();
         }
+    }
 
-        // clean up all interceptors by calling their destroy() method
+    /**
+     * Destroys all interceptors registered in the current configuration.
+     *
+     * @since 7.1.0
+     */
+    protected void destroyInterceptors() {
         Set<Interceptor> interceptors = new HashSet<>();
         Collection<PackageConfig> packageConfigs = 
configurationManager.getConfiguration().getPackageConfigs().values();
         for (PackageConfig packageConfig : packageConfigs) {
             for (Object config : 
packageConfig.getAllInterceptorConfigs().values()) {
-                if (config instanceof InterceptorStackConfig) {
-                    for (InterceptorMapping interceptorMapping : 
((InterceptorStackConfig) config).getInterceptors()) {
+                if (config instanceof InterceptorStackConfig isc) {
+                    for (InterceptorMapping interceptorMapping : 
isc.getInterceptors()) {
                         interceptors.add(interceptorMapping.getInterceptor());
                     }
                 }
@@ -480,16 +520,38 @@ public class Dispatcher {
         for (Interceptor interceptor : interceptors) {
             interceptor.destroy();
         }
+    }
 
-        // Clear container holder when application is unloaded / server 
shutdown
-        ContainerHolder.clear();
-
-        //cleanup action context
-        ActionContext.clear();
-
-        // clean up configuration
-        configurationManager.destroyConfiguration();
-        configurationManager = null;
+    /**
+     * Discovers and invokes all {@link InternalDestroyable} beans registered
+     * in the container, clearing static caches and stopping daemon threads
+     * to prevent classloader leaks during hot redeployment (WW-5537).
+     *
+     * <p>Beans implementing {@link ContextAwareDestroyable} receive the
+     * {@link jakarta.servlet.ServletContext} via
+     * {@link 
ContextAwareDestroyable#destroy(jakarta.servlet.ServletContext)}.</p>
+     *
+     * @since 7.1.0
+     */
+    protected void destroyInternalBeans() {
+        if (configurationManager != null && 
configurationManager.getConfiguration() != null) {
+            Container container = 
configurationManager.getConfiguration().getContainer();
+            Set<String> destroyableNames = 
container.getInstanceNames(InternalDestroyable.class);
+            for (String name : destroyableNames) {
+                try {
+                    InternalDestroyable destroyable = 
container.getInstance(InternalDestroyable.class, name);
+                    if (destroyable instanceof ContextAwareDestroyable cad) {
+                        cad.destroy(servletContext);
+                    } else {
+                        destroyable.destroy();
+                    }
+                } catch (Exception e) {
+                    LOG.warn("Error during internal cleanup [{}]", name, e);
+                }
+            }
+        } else {
+            LOG.warn("ConfigurationManager is null during cleanup, 
InternalDestroyable beans will not be invoked");
+        }
     }
 
     private void init_FileManager() throws ClassNotFoundException {

Reply via email to