You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@struts.apache.org by lu...@apache.org on 2026/03/23 09:14:46 UTC
(struts) 09/12: WW-5537 Dispatcher.cleanup: refactor into focused methods with InternalDestroyable discovery
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 <lu...@apache.org>
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 <no...@anthropic.com>
---
.../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 {