This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git
The following commit(s) were added to refs/heads/main by this push:
new 61c8fa25c WW-5832 Bound the FreeMarker template cache (#2090)
61c8fa25c is described below
commit 61c8fa25cecdea0093a58a98c26730586a49281e
Author: g0w6y <[email protected]>
AuthorDate: Sat Nov 7 13:38:39 2026 +0530
WW-5832 Bound the FreeMarker template cache (#2090)
With struts.freemarker.mru.max.strong.size=0 (default) the FreeMarker
template
cache stayed on the default SoftCacheStorage, which has no entry limit. Its
key
includes the locale, which for LocaleProvider actions derives from the
request,
so distinct request locales grew it without a count bound.
Use a SoftCacheStorage that is emptied once it holds 10000 entries. It stays
concurrent, so lookups remain lock-free (unlike MruCacheStorage, which
TemplateCache synchronizes on). The strong:N path is unchanged.
---
.../views/freemarker/FreemarkerManager.java | 25 ++++++++++++++++++++++
.../views/freemarker/FreemarkerManagerTest.java | 20 +++++++++++++++++
2 files changed, 45 insertions(+)
diff --git
a/core/src/main/java/org/apache/struts2/views/freemarker/FreemarkerManager.java
b/core/src/main/java/org/apache/struts2/views/freemarker/FreemarkerManager.java
index 3824a43c6..057f0ff3f 100644
---
a/core/src/main/java/org/apache/struts2/views/freemarker/FreemarkerManager.java
+++
b/core/src/main/java/org/apache/struts2/views/freemarker/FreemarkerManager.java
@@ -28,6 +28,7 @@ import org.apache.struts2.util.ValueStack;
import freemarker.cache.ClassTemplateLoader;
import freemarker.cache.FileTemplateLoader;
import freemarker.cache.MultiTemplateLoader;
+import freemarker.cache.SoftCacheStorage;
import freemarker.cache.TemplateLoader;
import freemarker.core.HTMLOutputFormat;
import freemarker.core.TemplateClassResolver;
@@ -161,6 +162,7 @@ public class FreemarkerManager {
private static final Logger LOG =
LogManager.getLogger(FreemarkerManager.class);
public static final String CONFIG_SERVLET_CONTEXT_KEY =
"freemarker.Configuration";
public static final String KEY_EXCEPTION = "exception";
+ static final int MAX_CACHED_TEMPLATES = 10000;
protected String templatePath;
@@ -335,6 +337,9 @@ public class FreemarkerManager {
if (mruMaxStrongSize > 0) {
LOG.debug("Sets Configuration.CACHE_STORAGE_KEY to strong:{}",
mruMaxStrongSize);
configuration.setSetting(Configuration.CACHE_STORAGE_KEY,
"strong:" + mruMaxStrongSize);
+ } else {
+ LOG.debug("Sets Configuration cache storage to a SoftCacheStorage
bounded to {} entries", MAX_CACHED_TEMPLATES);
+ configuration.setCacheStorage(new
BoundedSoftCacheStorage(MAX_CACHED_TEMPLATES));
}
if (templateUpdateDelay != null) {
LOG.debug("Sets Configuration.TEMPLATE_UPDATE_DELAY_KEY to {}",
templateUpdateDelay);
@@ -565,4 +570,24 @@ public class FreemarkerManager {
}
}
+ /**
+ * A {@link SoftCacheStorage} (concurrent, no locking on lookups) that is
emptied once it holds {@code maxSize} entries; the bound is approximate under
concurrency.
+ */
+ static class BoundedSoftCacheStorage extends SoftCacheStorage {
+
+ private final int maxSize;
+
+ BoundedSoftCacheStorage(int maxSize) {
+ this.maxSize = maxSize;
+ }
+
+ @Override
+ public void put(Object key, Object value) {
+ if (getSize() >= maxSize && get(key) == null) {
+ clear();
+ }
+ super.put(key, value);
+ }
+ }
+
}
diff --git
a/core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerManagerTest.java
b/core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerManagerTest.java
index 834983fca..cee2d9792 100644
---
a/core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerManagerTest.java
+++
b/core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerManagerTest.java
@@ -19,6 +19,8 @@
package org.apache.struts2.views.freemarker;
import org.apache.struts2.util.fs.DefaultFileManagerFactory;
+import freemarker.cache.CacheStorageWithGetSize;
+import freemarker.cache.ConcurrentCacheStorage;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.Version;
@@ -30,6 +32,7 @@ import jakarta.servlet.ServletContext;
import java.io.StringWriter;
import java.io.Writer;
import java.util.HashMap;
+import java.util.Locale;
import java.util.Map;
/**
@@ -138,6 +141,23 @@ public class FreemarkerManagerTest extends
StrutsInternalTestCase {
// then
assertFalse(manager.config.getWhitespaceStripping());
}
+
+ public void testTemplateCacheIsBoundedByDefault() throws Exception {
+ // given
+ FreemarkerManager manager = new FreemarkerManager();
+ container.inject(manager);
+ Configuration configuration = manager.getConfiguration(servletContext);
+
+ // when
+ for (int i = 0; i < FreemarkerManager.MAX_CACHED_TEMPLATES + 100; i++)
{
+
configuration.getTemplate("org/apache/struts2/views/freemarker/nested.ftl", new
Locale("xx", "YY", "v" + i));
+ }
+
+ // then
+ CacheStorageWithGetSize storage = (CacheStorageWithGetSize)
configuration.getCacheStorage();
+ assertTrue(storage.getSize() <=
FreemarkerManager.MAX_CACHED_TEMPLATES);
+ assertTrue(((ConcurrentCacheStorage) storage).isConcurrent());
+ }
}
class DummyFreemarkerManager extends FreemarkerManager {