diff --git a/core/src/main/java/com/opensymphony/xwork2/ognl/OgnlUtil.java b/core/src/main/java/com/opensymphony/xwork2/ognl/OgnlUtil.java index 007206cb8..c3eb840da 100644 --- a/core/src/main/java/com/opensymphony/xwork2/ognl/OgnlUtil.java +++ b/core/src/main/java/com/opensymphony/xwork2/ognl/OgnlUtil.java @@ -41,6 +41,7 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; @@ -57,8 +58,14 @@ public class OgnlUtil { // Flag used to reduce flooding logs with WARNs about using DevMode excluded packages private final AtomicBoolean warnReported = new AtomicBoolean(false); - private final ConcurrentMap expressions = new ConcurrentHashMap<>(); + private final AtomicBoolean useLRUExpressionCache = new AtomicBoolean(false); + private final AtomicBoolean useLRUBeanInfoCache = new AtomicBoolean(false); + private final AtomicInteger expressionsCacheMaxSize = new AtomicInteger(25000); + private final AtomicInteger beanInfoCacheMaxSize = new AtomicInteger(25000); + private final ConcurrentMap expressionsCache = new ConcurrentHashMap<>(); + private final LRUCache expressionsCacheLRU = new LRUCache<>(expressionsCacheMaxSize.get(), 16, 0.75f); private final ConcurrentMap, BeanInfo> beanInfoCache = new ConcurrentHashMap<>(); + private final LRUCache, BeanInfo> beanInfoCacheLRU = new LRUCache<>(beanInfoCacheMaxSize.get(), 16, 0.75f); private TypeConverter defaultConverter; private boolean devMode; @@ -103,6 +110,28 @@ public class OgnlUtil { enableExpressionCache = BooleanUtils.toBoolean(cache); } + @Inject(value = StrutsConstants.STRUTS_OGNL_EXPRESSION_CACHE_MAXSIZE, required = false) + protected void setExpressionsCacheMaxSize(String maxSize) { + expressionsCacheMaxSize.set(Integer.parseInt(maxSize)); + expressionsCacheLRU.setEvictionLimit(expressionsCacheMaxSize.get()); + } + + @Inject(value = StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_MAXSIZE, required = false) + protected void setBeanInfoCacheMaxSize(String maxSize) { + beanInfoCacheMaxSize.set(Integer.parseInt(maxSize)); + beanInfoCacheLRU.setEvictionLimit(beanInfoCacheMaxSize.get()); + } + + @Inject(value = StrutsConstants.STRUTS_OGNL_EXPRESSION_CACHE_LRU_MODE, required = false) + protected void setUseLRUExpressionCache(String useLRUMode) { + useLRUExpressionCache.set(BooleanUtils.toBoolean(useLRUMode)); + } + + @Inject(value = StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_LRU_MODE, required = false) + protected void setUseLRUBeanInfoCache(String useLRUMode) { + useLRUBeanInfoCache.set(BooleanUtils.toBoolean(useLRUMode)); + } + @Inject(value = StrutsConstants.STRUTS_OGNL_ENABLE_EVAL_EXPRESSION, required = false) protected void setEnableEvalExpression(String evalExpression) { this.enableEvalExpression = BooleanUtils.toBoolean(evalExpression); @@ -273,7 +302,8 @@ public class OgnlUtil { * @since 2.5.21 */ public void clearExpressionCache() { - expressions.clear(); + expressionsCache.clear(); + expressionsCacheLRU.clear(); } /** @@ -284,7 +314,11 @@ public class OgnlUtil { * @since 2.5.21 */ public int expressionCacheSize() { - return expressions.size(); + if (useLRUExpressionCache.get()) { + return expressionsCacheLRU.size(); + } else { + return expressionsCache.size(); + } } /** @@ -302,6 +336,7 @@ public class OgnlUtil { */ public void clearBeanInfoCache() { beanInfoCache.clear(); + beanInfoCacheLRU.clear(); } /** @@ -312,7 +347,11 @@ public class OgnlUtil { * @since 2.5.21 */ public int beanInfoCacheSize() { - return beanInfoCache.size(); + if (useLRUBeanInfoCache.get()) { + return beanInfoCacheLRU.size(); + } else { + return beanInfoCache.size(); + } } /** @@ -532,11 +571,18 @@ public class OgnlUtil { private Object compileAndExecute(String expression, Map context, OgnlTask task) throws OgnlException { Object tree; if (enableExpressionCache) { - tree = expressions.get(expression); + final Map expressionsCacheRef = (useLRUExpressionCache.get() ? expressionsCacheLRU.backingMapReference() : expressionsCache); + tree = expressionsCacheRef.get(expression); if (tree == null) { tree = Ognl.parseExpression(expression); checkEnableEvalExpression(tree, context); - expressions.putIfAbsent(expression, tree); + expressionsCacheRef.putIfAbsent(expression, tree); + if (! useLRUExpressionCache.get()) { + // If using a standard cache, after attempting to add an expression check cache size against the limit. + if (expressionsCacheRef.size() > expressionsCacheMaxSize.get()) { + expressionsCacheRef.clear(); // Non-LRU cache has exceeded maximum configured size, so flush. + } + } } } else { tree = Ognl.parseExpression(expression); @@ -549,11 +595,18 @@ public class OgnlUtil { private Object compileAndExecuteMethod(String expression, Map context, OgnlTask task) throws OgnlException { Object tree; if (enableExpressionCache) { - tree = expressions.get(expression); + final Map expressionsCacheRef = (useLRUExpressionCache.get() ? expressionsCacheLRU.backingMapReference() : expressionsCache); + tree = expressionsCacheRef.get(expression); if (tree == null) { tree = Ognl.parseExpression(expression); checkSimpleMethod(tree, context); - expressions.putIfAbsent(expression, tree); + expressionsCacheRef.putIfAbsent(expression, tree); + if (! useLRUExpressionCache.get()) { + // If using a standard cache, after attempting to add an expression check cache size against the limit. + if (expressionsCacheRef.size() > expressionsCacheMaxSize.get()) { + expressionsCacheRef.clear(); // Non-LRU cache has exceeded maximum configured size, so flush. + } + } } } else { tree = Ognl.parseExpression(expression); @@ -759,11 +812,18 @@ public class OgnlUtil { * @throws IntrospectionException is thrown if an exception occurs during introspection. */ public BeanInfo getBeanInfo(Class clazz) throws IntrospectionException { - synchronized (beanInfoCache) { - BeanInfo beanInfo = beanInfoCache.get(clazz); + final Map, BeanInfo> beanInfoCacheRef = (useLRUBeanInfoCache.get() ? beanInfoCacheLRU.backingMapReference() : beanInfoCache); + synchronized (beanInfoCacheRef) { + BeanInfo beanInfo = beanInfoCacheRef.get(clazz); if (beanInfo == null) { beanInfo = Introspector.getBeanInfo(clazz, Object.class); - beanInfoCache.putIfAbsent(clazz, beanInfo); + beanInfoCacheRef.putIfAbsent(clazz, beanInfo); + if (! useLRUBeanInfoCache.get()) { + // If using a standard cache, after attempting to add beanInfo check cache size against the limit. + if (beanInfoCacheRef.size() > beanInfoCacheMaxSize.get()) { + beanInfoCacheRef.clear(); // Non-LRU cache has exceeded maximum configured size, so flush. + } + } } return beanInfo; } @@ -822,4 +882,66 @@ public class OgnlUtil { T execute(Object tree) throws OgnlException; } + /** + * A basic LRUCache implementation that utilizes a {@link Collections#synchronizedMap(java.util.Map)} + * backed by a {@link LinkedHashMap}. May be replaced by a more efficient implementation in the future. + * + * @param Key type for the LRUCache + * @param Value type for the LRUCache + */ + protected class LRUCache { + private final Map lruCache; + private final AtomicInteger cacheEvictionLimit = new AtomicInteger(25000); + + public LRUCache(int evictionLimit, int initialCapacity, float loadFactor) { + this.cacheEvictionLimit.set(evictionLimit); + // Access-order mode selected (order mode true in LinkedHashMap constructor). + lruCache = Collections.synchronizedMap (new LinkedHashMap(initialCapacity, loadFactor, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return (this.size() > cacheEvictionLimit.get()); + } + }); + } + + public Value get(Key key) { + return lruCache.get(key); + } + + public void put(Key key, Value value) { + lruCache.put(key, value); + } + + public void putIfAbsent(Key key, Value value) { + lruCache.putIfAbsent(key, value); + } + + public int size() { + return lruCache.size(); + } + + public void clear() { + lruCache.clear(); + } + + /** + * Since the {@link LRUCache} is only intended to be used internally, we can cheat a bit + * and allow access to the backing map directly via its reference. Allows it to be used + * like the other {@link Map} caches within the module. + * + * @return The Map that backs the LRU cache. + */ + public Map backingMapReference() { + return lruCache; + } + + public int getEvictionLimit() { + return this.cacheEvictionLimit.get(); + } + + public void setEvictionLimit(int cacheEvictionLimit) { + this.cacheEvictionLimit.set(cacheEvictionLimit); + } + + } } diff --git a/core/src/main/java/org/apache/struts2/StrutsConstants.java b/core/src/main/java/org/apache/struts2/StrutsConstants.java index a46653d94..ba6ff87ff 100644 --- a/core/src/main/java/org/apache/struts2/StrutsConstants.java +++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java @@ -249,6 +249,31 @@ public final class StrutsConstants { /** Throw RuntimeException when a property is not found, or the evaluation of the expression fails */ public static final String STRUTS_EL_THROW_EXCEPTION = "struts.el.throwExceptionOnFailure"; + /** + * Specifies a maximum number of cached BeanInfo used by OgnlUtility. Not specified/set by default. If + * a positive integer is specified, it will set a limit whose behaviour depends on whether the + * normal (default) cache or optional LRU cache is in place. + * + * For the normal (default) cache, exceeding the maximum will cause the entire cache to flush (clear). + * For the optional LRU cache, once the maximum is reached, the least-recently-used (LRU) entry will be + * removed when a new entry needs to be added (cache is fully-utilized). + * + * @since 2.6 + */ + public static final String STRUTS_OGNL_BEANINFO_CACHE_MAXSIZE = "struts.ognl.beanInfoCacheMaxSize"; + + /** + * Set the cache mode of the BeanInfo cache used by OgnlUtility. A value of true means enable + * least-recently-used (LRU) mode, a value of false (or any non-true value) means to use the + * default cache. + * + * Note: When enabling LRU cache mode you must also set a maximum size (via {@link #STRUTS_OGNL_BEANINFO_CACHE_MAXSIZE}) + * for it to be effective. Otherwise, there is no condition to evict a LRU entry (cache has no limit). + * + * @since 2.6 + */ + public static final String STRUTS_OGNL_BEANINFO_CACHE_LRU_MODE = "struts.ognl.beanInfoCacheLRUMode"; + /** * Logs properties that are not found (very verbose) * @since 2.6 @@ -274,6 +299,31 @@ public final class StrutsConstants { */ public static final String STRUTS_ENABLE_OGNL_EXPRESSION_CACHE = STRUTS_OGNL_ENABLE_EXPRESSION_CACHE; + /** + * Specifies a maximum number of cached parsed OGNL expressions. Not specified/set by default. If + * a positive integer is specified, it will set a limit whose behaviour depends on whether the + * normal (default) cache or optional LRU cache is in place. + * + * For the normal (default) cache, exceeding the maximum will cause the entire cache to flush (clear). + * For the optional LRU cache, once the maximum is reached, the least-recently-used (LRU) entry will be + * removed when a new entry needs to be added (cache is fully-utilized). + * + * @since 2.6 + */ + public static final String STRUTS_OGNL_EXPRESSION_CACHE_MAXSIZE = "struts.ognl.expressionCacheMaxSize"; + + /** + * Set the cache mode of the parsed OGNL expression cache. A value of true means enable + * least-recently-used (LRU) mode, a value of false (or any non-true value) means to use the + * default cache. + * + * Note: When enabling LRU cache mode you must also set a maximum size (via {@link #STRUTS_OGNL_EXPRESSION_CACHE_MAXSIZE}) + * for it to be effective. Otherwise, there is no condition to evict a LRU entry (cache has no limit). + * + * @since 2.6 + */ + public static final String STRUTS_OGNL_EXPRESSION_CACHE_LRU_MODE = "struts.ognl.expressionCacheLRUMode"; + /** * Enables evaluation of OGNL expressions * @since 2.6 diff --git a/core/src/main/resources/org/apache/struts2/default.properties b/core/src/main/resources/org/apache/struts2/default.properties index df4069400..b88b6030d 100644 --- a/core/src/main/resources/org/apache/struts2/default.properties +++ b/core/src/main/resources/org/apache/struts2/default.properties @@ -224,6 +224,32 @@ struts.ognl.logMissingProperties=false ### if the application generates a lot of different expressions struts.ognl.enableExpressionCache=true +### Specify a limit to the number of entries in the OGNL expressionCache. +### For the standard expressionCache mode, when the limit is exceeded the entire cache's +### content will be cleared (can help prevent memory leaks). +### For expressionCacheLRUMode true, the limit will ensure the cache does not exceed +### that size, dropping the oldest (least-recently-used) expressions to add new ones. +### NOTE: If not set, the default is 25000, which may be excessive. +#struts.ognl.expressionCacheMaxSize=1000 + +### Indicates if the OGNL expressionCache should use LRU mode. +### NOTE: When true, make sure to set the expressionCacheMaxSize to a reasonable value +### for your application. Otherwise the default limit will never (practically) be reached. +#struts.ognl.expressionCacheLRUMode=false + +### Specify a limit to the number of entries in the OGNL beanInfoCache. +### For the standard beanInfoCache mode, when the limit is exceeded the entire cache's +### content will be cleared (can help prevent memory leaks). +### For beanInfoCacheLRUMode true, the limit will ensure the cache does not exceed +### that size, dropping the oldest (least-recently-used) expressions to add new ones. +### NOTE: If not set, the default is 25000, which may be excessive. +#struts.ognl.beanInfoCacheMaxSize=1000 + +### Indicates if the OGNL beanInfoCache should use LRU mode. +### NOTE: When true, make sure to set the beanInfoCacheMaxSize to a reasonable value +### for your application. Otherwise the default limit will never (practically) be reached. +#struts.ognl.beanInfoCacheLRUMode=false + ### Indicates if Dispatcher should handle unexpected exceptions by calling sendError() ### or simply rethrow it as a ServletException to allow future processing by other frameworks like Spring Security struts.handle.exception=true diff --git a/core/src/test/java/com/opensymphony/xwork2/ognl/OgnlUtilTest.java b/core/src/test/java/com/opensymphony/xwork2/ognl/OgnlUtilTest.java index 4b2f1ee83..8cebb1670 100644 --- a/core/src/test/java/com/opensymphony/xwork2/ognl/OgnlUtilTest.java +++ b/core/src/test/java/com/opensymphony/xwork2/ognl/OgnlUtilTest.java @@ -129,6 +129,62 @@ public class OgnlUtilTest extends XWorkTestCase { assertSame(expr0, expr2); } + public void testCacheEnabledMaxSize() throws OgnlException { + ognlUtil.setEnableExpressionCache("true"); + ognlUtil.setExpressionsCacheMaxSize("1"); + Object expr0 = ognlUtil.compile("test"); + Object expr2 = ognlUtil.compile("test"); + assertSame(expr0, expr2); + assertEquals("Expression cache size should be at its limit", 1, ognlUtil.expressionCacheSize()); + // Next epxression cached should cause the cache to clear (exceeding maximum sized). + Object expr3 = ognlUtil.compile("test1"); + assertEquals("Expression cache should be empty", 0, ognlUtil.expressionCacheSize()); + Object expr4 = ognlUtil.compile("test1"); + Object expr5 = ognlUtil.compile("test1"); + assertEquals("Expression cache size should still be at its limit", 1, ognlUtil.expressionCacheSize()); + assertNotSame("2nd test expression cache attempt will exceed size and force clear, but somehow they match ?", expr3, expr4); + assertSame(expr4, expr5); + // Next epxression cached should cause the cache to clear (exceeding maximum sized). + Object expr6 = ognlUtil.compile("test"); + assertEquals("Expression cache should be empty", 0, ognlUtil.expressionCacheSize()); + Object expr7 = ognlUtil.compile("test"); + Object expr8 = ognlUtil.compile("test"); + assertNotSame("2nd test expression cache attempt will exceed size and force clear, but somehow they match ?", expr6, expr7); + assertSame(expr7, expr8); + assertEquals("Expression LRU cache size should still be at its limit", 1, ognlUtil.expressionCacheSize()); + assertNotSame("1st test expression identical after ejection from LRU cache ?", expr5, expr0); + ognlUtil.setUseLRUExpressionCache("false"); + } + + public void testLRUCacheEnabled() throws OgnlException { + ognlUtil.setEnableExpressionCache("true"); + ognlUtil.setUseLRUExpressionCache("true"); + Object expr0 = ognlUtil.compile("test"); + Object expr2 = ognlUtil.compile("test"); + assertSame(expr0, expr2); + ognlUtil.setUseLRUExpressionCache("false"); + } + + public void testLRUCacheEnabledMaxSize() throws OgnlException { + ognlUtil.setEnableExpressionCache("true"); + ognlUtil.setUseLRUExpressionCache("true"); + ognlUtil.setExpressionsCacheMaxSize("1"); + Object expr0 = ognlUtil.compile("test"); + Object expr2 = ognlUtil.compile("test"); + assertSame(expr0, expr2); + assertEquals("Expression LRU cache size should be at its limit", 1, ognlUtil.expressionCacheSize()); + Object expr3 = ognlUtil.compile("test1"); + Object expr4 = ognlUtil.compile("test1"); + assertSame(expr3, expr4); + assertEquals("Expression LRU cache size should still be at its limit", 1, ognlUtil.expressionCacheSize()); + Object expr5 = ognlUtil.compile("test"); + Object expr6 = ognlUtil.compile("test"); + assertSame(expr5, expr6); + assertEquals("Expression LRU cache size should still be at its limit", 1, ognlUtil.expressionCacheSize()); + assertNotSame("1st test expression identical after ejection from LRU cache ?", expr5, expr0); + ognlUtil.setUseLRUExpressionCache("false"); + } + public void testExpressionIsCachedIrrespectiveOfItsExecutionStatus() throws OgnlException { Foo foo = new Foo(); OgnlContext context = (OgnlContext) ognlUtil.createDefaultContext(foo); @@ -149,6 +205,29 @@ public class OgnlUtilTest extends XWorkTestCase { } } + public void testExpressionIsLRUCachedIrrespectiveOfItsExecutionStatus() throws OgnlException { + ognlUtil.setEnableExpressionCache("true"); + ognlUtil.setUseLRUExpressionCache("true"); + Foo foo = new Foo(); + OgnlContext context = (OgnlContext) ognlUtil.createDefaultContext(foo); + + // Expression which executes with success + try { + ognlUtil.getValue("@com.opensymphony.xwork2.ognl.OgnlUtilTest@STATIC_FINAL_PUBLIC_ATTRIBUTE", context, foo); + assertEquals("Successfully executed expression must have been cached", ognlUtil.expressionCacheSize(), 1); + } catch (Exception ex) { + fail("Expression execution should have succeeded here. Exception: " + ex); + } + // Expression which executes with failure + try { + ognlUtil.getValue("@com.opensymphony.xwork2.ognl.OgnlUtilTest@STATIC_PRIVATE_ATTRIBUTE", context, foo); + fail("Expression execution should have failed here"); + } catch (Exception ex) { + assertEquals("Expression with failed execution must have been cached nevertheless", ognlUtil.expressionCacheSize(), 2); + } + ognlUtil.setUseLRUExpressionCache("false"); + } + public void testMethodExpressionIsCachedIrrespectiveOfItsExecutionStatus() throws Exception { Foo foo = new Foo(); OgnlContext context = (OgnlContext) ognlUtil.createDefaultContext(foo); @@ -193,6 +272,32 @@ public class OgnlUtilTest extends XWorkTestCase { assertTrue("Expression cache empty after usage ?", ognlUtil.expressionCacheSize() > 0); } + public void testClearExpressionLRUCache() throws OgnlException { + ognlUtil.setEnableExpressionCache("true"); + ognlUtil.setUseLRUExpressionCache("true"); + // Test that the expression cache is functioning as expected. + Object expr0 = ognlUtil.compile("test"); + Object expr1 = ognlUtil.compile("test"); + Object expr2 = ognlUtil.compile("test"); + // Cache in effect, so expr0, expr1, expr2 should be the same. + assertSame(expr0, expr1); + assertSame(expr0, expr2); + assertTrue("Expression cache empty before clear ?", ognlUtil.expressionCacheSize() > 0); + // Clear the Epxression cache and confirm subsequent requests are new. + ognlUtil.clearExpressionCache(); + assertEquals("Expression cache not empty after clear ?", 0, ognlUtil.expressionCacheSize()); + Object expr3 = ognlUtil.compile("test"); + Object expr4 = ognlUtil.compile("test"); + Object expr5 = ognlUtil.compile("test"); + // Cache cleared, expr3 should be a new instance. + assertNotSame(expr0, expr3); + // Cache still in effect, so expr3, expr4, expr5 should be the same. + assertSame(expr3, expr4); + assertSame(expr3, expr5); + assertTrue("Expression cache empty after usage ?", ognlUtil.expressionCacheSize() > 0); + ognlUtil.setUseLRUExpressionCache("false"); + } + public void testClearBeanInfoCache() throws IntrospectionException { final TestBean1 testBean1 = new TestBean1(); final TestBean2 testBean2 = new TestBean2(); @@ -242,6 +347,79 @@ public class OgnlUtilTest extends XWorkTestCase { assertTrue("BeanInfo cache empty after usage ?", ognlUtil.beanInfoCacheSize() > 0); } + public void testBeanInfoCache() throws IntrospectionException { + final TestBean1 testBean1 = new TestBean1(); + final TestBean2 testBean2 = new TestBean2(); + // Test that the BeanInfo cache is functioning as expected. + Object beanInfo1_1 = ognlUtil.getBeanInfo(testBean1); + Object beanInfo1_2 = ognlUtil.getBeanInfo(testBean1); + Object beanInfo1_3 = ognlUtil.getBeanInfo(testBean1); + // Cache in effect, so beanInfo1_1, beanInfo1_2, beanInfo1_3 should be the same. + assertSame(beanInfo1_1, beanInfo1_2); + assertSame(beanInfo1_1, beanInfo1_3); + Object beanInfo2_1 = ognlUtil.getBeanInfo(testBean2); + Object beanInfo2_2 = ognlUtil.getBeanInfo(testBean2); + Object beanInfo2_3 = ognlUtil.getBeanInfo(testBean2); + // Cache in effect, so beanInfo2_1, beanInfo2_2, beanInfo2_3 should be the same. + assertSame(beanInfo2_1, beanInfo2_2); + assertSame(beanInfo2_1, beanInfo2_3); + // BeanInfo for TestBean1 and TestBean2 should always be different. + assertNotSame(beanInfo1_1, beanInfo2_1); + assertTrue("BeanInfo cache empty after usage ?", ognlUtil.beanInfoCacheSize() > 0); + } + + public void testBeanInfoLRUCache() throws IntrospectionException { + ognlUtil.setUseLRUBeanInfoCache("true"); + final TestBean1 testBean1 = new TestBean1(); + final TestBean2 testBean2 = new TestBean2(); + // Test that the BeanInfo cache is functioning as expected. + Object beanInfo1_1 = ognlUtil.getBeanInfo(testBean1); + Object beanInfo1_2 = ognlUtil.getBeanInfo(testBean1); + Object beanInfo1_3 = ognlUtil.getBeanInfo(testBean1); + // Cache in effect, so beanInfo1_1, beanInfo1_2, beanInfo1_3 should be the same. + assertSame(beanInfo1_1, beanInfo1_2); + assertSame(beanInfo1_1, beanInfo1_3); + Object beanInfo2_1 = ognlUtil.getBeanInfo(testBean2); + Object beanInfo2_2 = ognlUtil.getBeanInfo(testBean2); + Object beanInfo2_3 = ognlUtil.getBeanInfo(testBean2); + // Cache in effect, so beanInfo2_1, beanInfo2_2, beanInfo2_3 should be the same. + assertSame(beanInfo2_1, beanInfo2_2); + assertSame(beanInfo2_1, beanInfo2_3); + // BeanInfo for TestBean1 and TestBean2 should always be different. + assertNotSame(beanInfo1_1, beanInfo2_1); + assertTrue("BeanInfo cache empty after usage ?", ognlUtil.beanInfoCacheSize() > 0); + ognlUtil.setUseLRUBeanInfoCache("true"); + } + + public void testBeanInfoLRUCacheLimits() throws IntrospectionException { + ognlUtil.setUseLRUBeanInfoCache("true"); + ognlUtil.setBeanInfoCacheMaxSize("1"); + final TestBean1 testBean1 = new TestBean1(); + final TestBean2 testBean2 = new TestBean2(); + // Test that the BeanInfo cache is functioning as expected. + Object beanInfo1_1 = ognlUtil.getBeanInfo(testBean1); + Object beanInfo1_2 = ognlUtil.getBeanInfo(testBean1); + Object beanInfo1_3 = ognlUtil.getBeanInfo(testBean1); + // Cache in effect, so beanInfo1_1, beanInfo1_2, beanInfo1_3 should be the same. + assertSame(beanInfo1_1, beanInfo1_2); + assertSame(beanInfo1_1, beanInfo1_3); + Object beanInfo2_1 = ognlUtil.getBeanInfo(testBean2); + Object beanInfo2_2 = ognlUtil.getBeanInfo(testBean2); + Object beanInfo2_3 = ognlUtil.getBeanInfo(testBean2); + // Cache in effect, so beanInfo2_1, beanInfo2_2, beanInfo2_3 should be the same. + assertSame(beanInfo2_1, beanInfo2_2); + assertSame(beanInfo2_1, beanInfo2_3); + // BeanInfo for TestBean1 and TestBean2 should always be different. + assertNotSame(beanInfo1_1, beanInfo2_1); + assertTrue("BeanInfo cache empty after usage ?", ognlUtil.beanInfoCacheSize() > 0); + assertEquals("BeanInfo LRU cache size should be at its limit", 1, ognlUtil.beanInfoCacheSize()); + // LRU cache should not contain TestBean1 beaninfo anymore. A new entry should exist in the cache. + Object beanInfo1_4 = ognlUtil.getBeanInfo(testBean1); + assertNotSame("BeanInfo dropped from LRU cache is the same as newly added ?", beanInfo1_1, beanInfo1_4); + ognlUtil.setUseLRUBeanInfoCache("false"); + ognlUtil.setBeanInfoCacheMaxSize(String.valueOf(Integer.MAX_VALUE)); + } + public void testClearRuntimeCache() { // Confirm that no exceptions or failures arise when calling the convenience global clear method. OgnlUtil.clearRuntimeCache(); @@ -1525,6 +1703,36 @@ public class OgnlUtilTest extends XWorkTestCase { } } + public void testOgnlUtilLRUCacheClass() throws OgnlException { + OgnlUtil.LRUCache lruCache = ognlUtil.new LRUCache<>(2, 16, 0.75f); + Map backingMap = lruCache.backingMapReference(); + assertNotNull("Backing Map somehow null ?", backingMap); + assertEquals("Initial evictionLimit did not match initial value", 2, lruCache.getEvictionLimit()); + lruCache.setEvictionLimit(3); + assertEquals("Updated evictionLimit did not match updated value", 3, lruCache.getEvictionLimit()); + String lookupResult = lruCache.get(Integer.valueOf(0)); + assertNull("Lookup of empty cache returned non-null value ?", lookupResult); + lruCache.put(Integer.valueOf(0), "Zero"); + lookupResult = lruCache.get(Integer.valueOf(0)); + assertEquals("Retrieved value does not match put value ?", "Zero", lookupResult); + lruCache.put(Integer.valueOf(1), "One"); + lruCache.put(Integer.valueOf(2), "Two"); + assertEquals("LRU cache not size evictionlimit after adding three values ?", lruCache.getEvictionLimit(), lruCache.size()); + lookupResult = lruCache.get(Integer.valueOf(2)); + assertEquals("Retrieved value does not match put value ?", "Two", lookupResult); + lruCache.put(Integer.valueOf(3), "Three"); + assertEquals("LRU cache not size evictionlimit after adding values ?", lruCache.getEvictionLimit(), lruCache.size()); + lookupResult = lruCache.get(Integer.valueOf(0)); + assertNull("Lookup of value 0 (should have dropped off LRU cache) returned non-null value ?", lookupResult); + lruCache.putIfAbsent(Integer.valueOf(2), "Two"); + lookupResult = lruCache.get(Integer.valueOf(2)); + assertEquals("Retrieved value does not match put value ?", "Two", lookupResult); + assertEquals("LRUCache and backing map size different after puts ?", lruCache.size(), backingMap.size()); + lruCache.clear(); + assertEquals("LRU cache not empty after clear ?", 0, lruCache.size()); + assertEquals("LRUCache and backing map size different after clear ?", lruCache.size(), backingMap.size()); + } + private void reloadTestContainerConfiguration(boolean devMode, boolean allowStaticMethod) { loadConfigurationProviders(new StubConfigurationProvider() { @Override