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});
