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