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 {
