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 819703157fb8e02d88cd37b5183e0be7853a0e3a
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon Mar 23 09:57:39 2026 +0100

    WW-5537 Add InternalDestroyable adapter classes for static cache cleanup
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../org/apache/struts2/components/Component.java   |  8 ++++
 .../dispatcher/ComponentCacheDestroyable.java      | 35 ++++++++++++++
 .../dispatcher/DebugUtilsCacheDestroyable.java     | 35 ++++++++++++++
 .../FinalizableReferenceQueueDestroyable.java      | 35 ++++++++++++++
 .../dispatcher/FreemarkerCacheDestroyable.java     | 56 ++++++++++++++++++++++
 .../struts2/dispatcher/OgnlCacheDestroyable.java   | 38 +++++++++++++++
 .../ScopeInterceptorCacheDestroyable.java          | 35 ++++++++++++++
 .../java/org/apache/struts2/util/DebugUtils.java   |  7 +++
 8 files changed, 249 insertions(+)

diff --git a/core/src/main/java/org/apache/struts2/components/Component.java 
b/core/src/main/java/org/apache/struts2/components/Component.java
index c9cd1bd67..10e6373cc 100644
--- a/core/src/main/java/org/apache/struts2/components/Component.java
+++ b/core/src/main/java/org/apache/struts2/components/Component.java
@@ -68,6 +68,14 @@ public class Component {
      */
     protected static ConcurrentMap<Class<?>, Collection<String>> 
standardAttributesMap = new ConcurrentHashMap<>();
 
+    /**
+     * Clears the standard attributes cache to prevent classloader memory 
leaks during hot redeployment.
+     * The cache uses Class keys which pin the webapp classloader.
+     */
+    public static void clearStandardAttributesMap() {
+        standardAttributesMap.clear();
+    }
+
     protected boolean devMode = false;
     protected boolean escapeHtmlBody = false;
     protected ValueStack stack;
diff --git 
a/core/src/main/java/org/apache/struts2/dispatcher/ComponentCacheDestroyable.java
 
b/core/src/main/java/org/apache/struts2/dispatcher/ComponentCacheDestroyable.java
new file mode 100644
index 000000000..0d81c7a98
--- /dev/null
+++ 
b/core/src/main/java/org/apache/struts2/dispatcher/ComponentCacheDestroyable.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.dispatcher;
+
+import org.apache.struts2.components.Component;
+
+/**
+ * Clears {@link Component}'s static standard attributes cache to prevent
+ * classloader leaks on hot redeploy.
+ *
+ * @since 7.1.0
+ */
+public class ComponentCacheDestroyable implements InternalDestroyable {
+
+    @Override
+    public void destroy() {
+        Component.clearStandardAttributesMap();
+    }
+}
diff --git 
a/core/src/main/java/org/apache/struts2/dispatcher/DebugUtilsCacheDestroyable.java
 
b/core/src/main/java/org/apache/struts2/dispatcher/DebugUtilsCacheDestroyable.java
new file mode 100644
index 000000000..d49f6f9a0
--- /dev/null
+++ 
b/core/src/main/java/org/apache/struts2/dispatcher/DebugUtilsCacheDestroyable.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.dispatcher;
+
+import org.apache.struts2.util.DebugUtils;
+
+/**
+ * Clears {@link DebugUtils}'s static logged-keys cache to prevent memory leaks
+ * during hot redeployment.
+ *
+ * @since 7.1.0
+ */
+public class DebugUtilsCacheDestroyable implements InternalDestroyable {
+
+    @Override
+    public void destroy() {
+        DebugUtils.clearCache();
+    }
+}
diff --git 
a/core/src/main/java/org/apache/struts2/dispatcher/FinalizableReferenceQueueDestroyable.java
 
b/core/src/main/java/org/apache/struts2/dispatcher/FinalizableReferenceQueueDestroyable.java
new file mode 100644
index 000000000..273f1ac5f
--- /dev/null
+++ 
b/core/src/main/java/org/apache/struts2/dispatcher/FinalizableReferenceQueueDestroyable.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.dispatcher;
+
+import org.apache.struts2.inject.util.FinalizableReferenceQueue;
+
+/**
+ * Adapter that exposes {@link FinalizableReferenceQueue#stopAndClear()} as an
+ * {@link InternalDestroyable} bean.
+ *
+ * @since 7.1.0
+ */
+public class FinalizableReferenceQueueDestroyable implements 
InternalDestroyable {
+
+    @Override
+    public void destroy() {
+        FinalizableReferenceQueue.stopAndClear();
+    }
+}
diff --git 
a/core/src/main/java/org/apache/struts2/dispatcher/FreemarkerCacheDestroyable.java
 
b/core/src/main/java/org/apache/struts2/dispatcher/FreemarkerCacheDestroyable.java
new file mode 100644
index 000000000..9ae024f10
--- /dev/null
+++ 
b/core/src/main/java/org/apache/struts2/dispatcher/FreemarkerCacheDestroyable.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.dispatcher;
+
+import freemarker.ext.beans.BeansWrapper;
+import freemarker.template.Configuration;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.struts2.views.freemarker.FreemarkerManager;
+
+import jakarta.servlet.ServletContext;
+
+/**
+ * WW-5537: Clears FreeMarker's template and class introspection caches
+ * stored in {@link ServletContext} during application undeploy, preventing
+ * classloader leaks.
+ *
+ * @since 7.1.0
+ */
+public class FreemarkerCacheDestroyable implements ContextAwareDestroyable {
+
+    private static final Logger LOG = 
LogManager.getLogger(FreemarkerCacheDestroyable.class);
+
+    @Override
+    public void destroy(ServletContext servletContext) {
+        if (servletContext == null) {
+            return;
+        }
+        Object fmConfig = 
servletContext.getAttribute(FreemarkerManager.CONFIG_SERVLET_CONTEXT_KEY);
+        if (fmConfig instanceof Configuration cfg) {
+            cfg.clearTemplateCache();
+            cfg.clearEncodingMap();
+            if (cfg.getObjectWrapper() instanceof BeansWrapper bw) {
+                bw.clearClassIntrospectionCache();
+            }
+            
servletContext.removeAttribute(FreemarkerManager.CONFIG_SERVLET_CONTEXT_KEY);
+            LOG.debug("FreeMarker configuration cleaned up");
+        }
+    }
+}
diff --git 
a/core/src/main/java/org/apache/struts2/dispatcher/OgnlCacheDestroyable.java 
b/core/src/main/java/org/apache/struts2/dispatcher/OgnlCacheDestroyable.java
new file mode 100644
index 000000000..463f93b80
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/dispatcher/OgnlCacheDestroyable.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.dispatcher;
+
+import org.apache.struts2.ognl.OgnlUtil;
+
+import java.beans.Introspector;
+
+/**
+ * Clears OGNL runtime caches and JDK introspection caches that hold
+ * {@code Class<?>} references, preventing classloader leaks on hot redeploy.
+ *
+ * @since 7.1.0
+ */
+public class OgnlCacheDestroyable implements InternalDestroyable {
+
+    @Override
+    public void destroy() {
+        OgnlUtil.clearRuntimeCache();
+        Introspector.flushCaches();
+    }
+}
diff --git 
a/core/src/main/java/org/apache/struts2/dispatcher/ScopeInterceptorCacheDestroyable.java
 
b/core/src/main/java/org/apache/struts2/dispatcher/ScopeInterceptorCacheDestroyable.java
new file mode 100644
index 000000000..39eaf615e
--- /dev/null
+++ 
b/core/src/main/java/org/apache/struts2/dispatcher/ScopeInterceptorCacheDestroyable.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.dispatcher;
+
+import org.apache.struts2.interceptor.ScopeInterceptor;
+
+/**
+ * Clears {@link ScopeInterceptor}'s static locks map to prevent classloader
+ * leaks on hot redeploy.
+ *
+ * @since 7.1.0
+ */
+public class ScopeInterceptorCacheDestroyable implements InternalDestroyable {
+
+    @Override
+    public void destroy() {
+        ScopeInterceptor.clearLocks();
+    }
+}
diff --git a/core/src/main/java/org/apache/struts2/util/DebugUtils.java 
b/core/src/main/java/org/apache/struts2/util/DebugUtils.java
index cc8ca5f5f..6938bb948 100644
--- a/core/src/main/java/org/apache/struts2/util/DebugUtils.java
+++ b/core/src/main/java/org/apache/struts2/util/DebugUtils.java
@@ -32,6 +32,13 @@ public final class DebugUtils {
 
     private static final Set<String> IS_LOGGED = ConcurrentHashMap.newKeySet();
 
+    /**
+     * Clears the logged-keys cache to prevent memory leaks during hot 
redeployment.
+     */
+    public static void clearCache() {
+        IS_LOGGED.clear();
+    }
+
     public static void notifyDeveloperOfError(Logger log, Object action, 
String message) {
         if (action instanceof TextProvider tp) {
             message = tp.getText("devmode.notification", "Developer 
Notification:\n{0}", new String[]{message});

Reply via email to