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

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

commit c418caf9972e291b1827bbeb9262bfe6ada30256
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sun Sep 13 17:06:04 2026 +0200

    WW-5687 feat(core): clear the conversion and validator caches on 
Dispatcher.cleanup()
    
    Follow-on of WW-5537. StrutsTypeConverterHolder and 
DefaultActionValidatorManager
    hold per-instance caches of application Class objects and 
converter/validator
    instances; they are now cleared with the other InternalDestroyable beans.
    
    The holders are not registered as InternalDestroyable themselves: the 
container
    keys singletons by (type, name), so a second registration would build a 
second,
    empty instance and clear nothing. Two adapter beans @Inject the in-use 
singleton
    (resolved through the StrutsBeanSelectionProvider alias, so a custom
    struts.actionValidatorManager or struts.converter.holder binding is 
covered) and
    call the new clearCache() default method, which is a no-op for third-party
    implementations.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 .../conversion/StrutsTypeConverterHolder.java      |  8 ++++
 .../struts2/conversion/TypeConverterHolder.java    |  9 +++++
 .../ActionValidatorManagerDestroyable.java         | 44 ++++++++++++++++++++
 .../dispatcher/TypeConverterHolderDestroyable.java | 44 ++++++++++++++++++++
 .../struts2/validator/ActionValidatorManager.java  |  9 +++++
 .../validator/DefaultActionValidatorManager.java   |  6 +++
 core/src/main/resources/struts-beans.xml           |  4 ++
 .../struts2/dispatcher/DispatcherCleanupTest.java  | 47 +++++++++++++++++++++-
 8 files changed, 170 insertions(+), 1 deletion(-)

diff --git 
a/core/src/main/java/org/apache/struts2/conversion/StrutsTypeConverterHolder.java
 
b/core/src/main/java/org/apache/struts2/conversion/StrutsTypeConverterHolder.java
index 4b3d5ca38..c685c037e 100644
--- 
a/core/src/main/java/org/apache/struts2/conversion/StrutsTypeConverterHolder.java
+++ 
b/core/src/main/java/org/apache/struts2/conversion/StrutsTypeConverterHolder.java
@@ -197,4 +197,12 @@ public class StrutsTypeConverterHolder implements 
TypeConverterHolder {
         unknownMappingsInternal.add(className);
     }
 
+    @Override
+    public void clearCache() {
+        defaultMappings.clear();
+        mappings.clear();
+        unknownMappingsInternal.clear();
+        unknownMappings.clear();
+    }
+
 }
diff --git 
a/core/src/main/java/org/apache/struts2/conversion/TypeConverterHolder.java 
b/core/src/main/java/org/apache/struts2/conversion/TypeConverterHolder.java
index 8fc1c507a..7c818085a 100644
--- a/core/src/main/java/org/apache/struts2/conversion/TypeConverterHolder.java
+++ b/core/src/main/java/org/apache/struts2/conversion/TypeConverterHolder.java
@@ -152,4 +152,13 @@ public interface TypeConverterHolder {
         return mapping;
     }
 
+    /**
+     * Drops every registered and cached mapping. Called during {@code 
Dispatcher.cleanup()};
+     * the default is a no-op so implementations that hold no state need not 
override it.
+     *
+     * @since 7.4.0
+     */
+    default void clearCache() {
+    }
+
 }
diff --git 
a/core/src/main/java/org/apache/struts2/dispatcher/ActionValidatorManagerDestroyable.java
 
b/core/src/main/java/org/apache/struts2/dispatcher/ActionValidatorManagerDestroyable.java
new file mode 100644
index 000000000..387bfd8e4
--- /dev/null
+++ 
b/core/src/main/java/org/apache/struts2/dispatcher/ActionValidatorManagerDestroyable.java
@@ -0,0 +1,44 @@
+/*
+ * 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.Inject;
+import org.apache.struts2.validator.ActionValidatorManager;
+
+/**
+ * Clears the {@link ActionValidatorManager} in use. Registered as a separate 
bean rather than
+ * making the manager itself an {@link InternalDestroyable}: the container 
keys singletons by
+ * (type, name), so a second registration of the manager class would build a 
second, empty instance.
+ *
+ * @since 7.4.0
+ */
+public class ActionValidatorManagerDestroyable implements InternalDestroyable {
+
+    private ActionValidatorManager actionValidatorManager;
+
+    @Inject
+    public void setActionValidatorManager(ActionValidatorManager 
actionValidatorManager) {
+        this.actionValidatorManager = actionValidatorManager;
+    }
+
+    @Override
+    public void destroy() {
+        actionValidatorManager.clearCache();
+    }
+}
diff --git 
a/core/src/main/java/org/apache/struts2/dispatcher/TypeConverterHolderDestroyable.java
 
b/core/src/main/java/org/apache/struts2/dispatcher/TypeConverterHolderDestroyable.java
new file mode 100644
index 000000000..77a4b67cd
--- /dev/null
+++ 
b/core/src/main/java/org/apache/struts2/dispatcher/TypeConverterHolderDestroyable.java
@@ -0,0 +1,44 @@
+/*
+ * 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.conversion.TypeConverterHolder;
+import org.apache.struts2.inject.Inject;
+
+/**
+ * Clears the {@link TypeConverterHolder} in use. Registered as a separate 
bean rather than making
+ * the holder itself an {@link InternalDestroyable}: the container keys 
singletons by (type, name),
+ * so a second registration of the holder class would build a second, empty 
instance.
+ *
+ * @since 7.4.0
+ */
+public class TypeConverterHolderDestroyable implements InternalDestroyable {
+
+    private TypeConverterHolder typeConverterHolder;
+
+    @Inject
+    public void setTypeConverterHolder(TypeConverterHolder 
typeConverterHolder) {
+        this.typeConverterHolder = typeConverterHolder;
+    }
+
+    @Override
+    public void destroy() {
+        typeConverterHolder.clearCache();
+    }
+}
diff --git 
a/core/src/main/java/org/apache/struts2/validator/ActionValidatorManager.java 
b/core/src/main/java/org/apache/struts2/validator/ActionValidatorManager.java
index 99356a6b3..4771f585b 100644
--- 
a/core/src/main/java/org/apache/struts2/validator/ActionValidatorManager.java
+++ 
b/core/src/main/java/org/apache/struts2/validator/ActionValidatorManager.java
@@ -87,4 +87,13 @@ public interface ActionValidatorManager {
      * @throws ValidationException if an error happens when validating the 
action.
      */
     void validate(Object object, String context, ValidatorContext 
validatorContext, String method) throws ValidationException;
+
+    /**
+     * Drops every cached validator configuration. Called during {@code 
Dispatcher.cleanup()};
+     * the default is a no-op so implementations that hold no state need not 
override it.
+     *
+     * @since 7.4.0
+     */
+    default void clearCache() {
+    }
 }
diff --git 
a/core/src/main/java/org/apache/struts2/validator/DefaultActionValidatorManager.java
 
b/core/src/main/java/org/apache/struts2/validator/DefaultActionValidatorManager.java
index b2ff42161..3bf118bd9 100644
--- 
a/core/src/main/java/org/apache/struts2/validator/DefaultActionValidatorManager.java
+++ 
b/core/src/main/java/org/apache/struts2/validator/DefaultActionValidatorManager.java
@@ -176,6 +176,12 @@ public class DefaultActionValidatorManager implements 
ActionValidatorManager {
         return getValidators(clazz, context, null);
     }
 
+    @Override
+    public void clearCache() {
+        validatorCache.clear();
+        validatorFileCache.clear();
+    }
+
     @Override
     public void validate(Object object, String context, ValidatorContext 
validatorContext, String method) throws ValidationException {
         List<Validator> validators = getValidators(object.getClass(), context, 
method);
diff --git a/core/src/main/resources/struts-beans.xml 
b/core/src/main/resources/struts-beans.xml
index e53710c22..d12535b9c 100644
--- a/core/src/main/resources/struts-beans.xml
+++ b/core/src/main/resources/struts-beans.xml
@@ -290,5 +290,9 @@
           class="org.apache.struts2.dispatcher.FreemarkerCacheDestroyable"/>
     <bean type="org.apache.struts2.dispatcher.InternalDestroyable" 
name="debugUtilsCache"
           class="org.apache.struts2.dispatcher.DebugUtilsCacheDestroyable"/>
+    <bean type="org.apache.struts2.dispatcher.InternalDestroyable" 
name="typeConverterHolder"
+          
class="org.apache.struts2.dispatcher.TypeConverterHolderDestroyable"/>
+    <bean type="org.apache.struts2.dispatcher.InternalDestroyable" 
name="actionValidatorManager"
+          
class="org.apache.struts2.dispatcher.ActionValidatorManagerDestroyable"/>
 
 </struts>
diff --git 
a/core/src/test/java/org/apache/struts2/dispatcher/DispatcherCleanupTest.java 
b/core/src/test/java/org/apache/struts2/dispatcher/DispatcherCleanupTest.java
index d55cb4438..c5ccbc74b 100644
--- 
a/core/src/test/java/org/apache/struts2/dispatcher/DispatcherCleanupTest.java
+++ 
b/core/src/test/java/org/apache/struts2/dispatcher/DispatcherCleanupTest.java
@@ -21,10 +21,14 @@ package org.apache.struts2.dispatcher;
 import org.apache.struts2.ActionContext;
 import org.apache.struts2.StrutsJUnit4InternalTestCase;
 import org.apache.struts2.components.Component;
+import org.apache.struts2.conversion.TypeConverterHolder;
 import org.apache.struts2.inject.Container;
 import org.apache.struts2.ognl.accessor.CompoundRootAccessor;
 import org.apache.struts2.util.DebugUtils;
 import org.apache.struts2.util.fs.DefaultFileManager;
+import org.apache.struts2.validator.ActionValidatorManager;
+import org.apache.struts2.validator.DefaultActionValidatorManager;
+import org.apache.struts2.validator.ValidatorConfig;
 import org.junit.Test;
 
 import java.lang.reflect.Field;
@@ -58,7 +62,7 @@ public class DispatcherCleanupTest extends 
StrutsJUnit4InternalTestCase {
         Set<String> expected = new HashSet<>(Arrays.asList(
                 "componentCache", "compoundRootAccessor", "defaultFileManager",
                 "scopeInterceptorCache", "ognlCache", 
"finalizableReferenceQueue",
-                "freemarkerCache", "debugUtilsCache"
+                "freemarkerCache", "debugUtilsCache", "typeConverterHolder", 
"actionValidatorManager"
         ));
         assertThat(names).containsAll(expected);
     }
@@ -132,6 +136,47 @@ public class DispatcherCleanupTest extends 
StrutsJUnit4InternalTestCase {
         assertThat(lazyCache).isEmpty();
     }
 
+    @Test
+    public void cleanupClearsTypeConverterHolderCaches() {
+        initDispatcher(emptyMap());
+
+        Container container = 
dispatcher.getConfigurationManager().getConfiguration().getContainer();
+        TypeConverterHolder holder = 
container.getInstance(TypeConverterHolder.class);
+        holder.addMapping(DispatcherCleanupTest.class, Map.of("field", new 
Object()));
+        holder.addUnknownMapping(DispatcherCleanupTest.class.getName());
+        assertThat(holder.getMapping(DispatcherCleanupTest.class)).isNotNull();
+        
assertThat(holder.containsUnknownMapping(DispatcherCleanupTest.class.getName())).isTrue();
+
+        dispatcher.cleanup();
+
+        assertThat(holder.getMapping(DispatcherCleanupTest.class)).isNull();
+        
assertThat(holder.containsUnknownMapping(DispatcherCleanupTest.class.getName())).isFalse();
+    }
+
+    @Test
+    public void cleanupClearsActionValidatorManagerCaches() throws Exception {
+        initDispatcher(emptyMap());
+
+        Container container = 
dispatcher.getConfigurationManager().getConfiguration().getContainer();
+        ActionValidatorManager manager = 
container.getInstance(ActionValidatorManager.class);
+        Map<String, List<ValidatorConfig>> validatorCache = 
instanceMap(manager, "validatorCache");
+        Map<String, List<ValidatorConfig>> validatorFileCache = 
instanceMap(manager, "validatorFileCache");
+        validatorCache.put("test-key", new ArrayList<>());
+        validatorFileCache.put("test-file", new ArrayList<>());
+
+        dispatcher.cleanup();
+
+        assertThat(validatorCache).isEmpty();
+        assertThat(validatorFileCache).isEmpty();
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Map<String, List<ValidatorConfig>> 
instanceMap(ActionValidatorManager manager, String fieldName) throws Exception {
+        Field field = 
DefaultActionValidatorManager.class.getDeclaredField(fieldName);
+        field.setAccessible(true);
+        return (Map<String, List<ValidatorConfig>>) field.get(manager);
+    }
+
     @Test
     public void cleanupClearsDispatcherListeners() throws Exception {
         initDispatcher(emptyMap());

Reply via email to