- Refactored the cache design to utilize a factory pattern.
- Updated unit tests to match refactoring.
This commit is contained in:
JCgH4164838Gh792C124B5
2022-03-06 21:18:21 -05:00
parent 4d8108e766
commit fbb31ee65b
14 changed files with 633 additions and 154 deletions
@@ -28,6 +28,9 @@ import com.opensymphony.xwork2.conversion.*;
import com.opensymphony.xwork2.conversion.impl.*;
import com.opensymphony.xwork2.factory.*;
import com.opensymphony.xwork2.inject.*;
import com.opensymphony.xwork2.ognl.DefaultOgnlBeanInfoCacheFactory;
import com.opensymphony.xwork2.ognl.DefaultOgnlExpressionCacheFactory;
import com.opensymphony.xwork2.ognl.OgnlCacheFactory;
import com.opensymphony.xwork2.ognl.OgnlReflectionProvider;
import com.opensymphony.xwork2.ognl.OgnlUtil;
import com.opensymphony.xwork2.ognl.OgnlValueStackFactory;
@@ -283,6 +286,9 @@ public class DefaultConfiguration implements Configuration {
builder.factory(ValueSubstitutor.class, EnvsValueSubstitutor.class, Scope.SINGLETON);
builder.factory(OgnlCacheFactory.class, StrutsConstants.STRUTS_OGNL_EXPRESSIONCACHE_FACTORY, DefaultOgnlExpressionCacheFactory.class, Scope.SINGLETON);
builder.factory(OgnlCacheFactory.class, StrutsConstants.STRUTS_OGNL_BEANINFOCACHE_FACTORY, DefaultOgnlBeanInfoCacheFactory.class, Scope.SINGLETON);
builder.constant(StrutsConstants.STRUTS_DEVMODE, "false");
builder.constant(StrutsConstants.STRUTS_OGNL_LOG_MISSING_PROPERTIES, "false");
builder.constant(StrutsConstants.STRUTS_OGNL_ENABLE_EVAL_EXPRESSION, "false");
@@ -91,6 +91,9 @@ import com.opensymphony.xwork2.ognl.accessor.XWorkMapPropertyAccessor;
import com.opensymphony.xwork2.ognl.accessor.XWorkMethodAccessor;
import com.opensymphony.xwork2.util.CompoundRoot;
import com.opensymphony.xwork2.LocalizedTextProvider;
import com.opensymphony.xwork2.ognl.DefaultOgnlBeanInfoCacheFactory;
import com.opensymphony.xwork2.ognl.DefaultOgnlExpressionCacheFactory;
import com.opensymphony.xwork2.ognl.OgnlCacheFactory;
import com.opensymphony.xwork2.util.StrutsLocalizedTextProvider;
import com.opensymphony.xwork2.util.OgnlTextParser;
import com.opensymphony.xwork2.util.PatternMatcher;
@@ -127,19 +130,24 @@ import java.util.Set;
public class StrutsDefaultConfigurationProvider implements ConfigurationProvider {
@Override
public void destroy() {
}
@Override
public void init(Configuration configuration) throws ConfigurationException {
}
@Override
public void loadPackages() throws ConfigurationException {
}
@Override
public boolean needsReload() {
return false;
}
@Override
public void register(ContainerBuilder builder, LocatableProperties props)
throws ConfigurationException {
@@ -218,6 +226,9 @@ public class StrutsDefaultConfigurationProvider implements ConfigurationProvider
, Scope.SINGLETON)
.factory(ValueSubstitutor.class, EnvsValueSubstitutor.class, Scope.SINGLETON)
.factory(OgnlCacheFactory.class, StrutsConstants.STRUTS_OGNL_EXPRESSIONCACHE_FACTORY, DefaultOgnlExpressionCacheFactory.class, Scope.SINGLETON)
.factory(OgnlCacheFactory.class, StrutsConstants.STRUTS_OGNL_BEANINFOCACHE_FACTORY, DefaultOgnlBeanInfoCacheFactory.class, Scope.SINGLETON)
;
props.setProperty(StrutsConstants.STRUTS_ENABLE_DYNAMIC_METHOD_INVOCATION, Boolean.FALSE.toString());
@@ -0,0 +1,43 @@
/*
* Copyright 2022 Apache Software Foundation.
*
* Licensed 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 com.opensymphony.xwork2.ognl;
import com.opensymphony.xwork2.inject.Inject;
import org.apache.struts2.StrutsConstants;
/**
* Default OGNL Cache factory implementation.
*
* Currently used for BeanInfo cache creation.
*
* @param <Key> The type for the cache key entries
* @param <Value> The type for the cache value entries
*/
public class DefaultOgnlBeanInfoCacheFactory<Key, Value> extends DefaultOgnlCacheFactory {
@Override
@Inject(value = StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_MAXSIZE, required = false)
protected void setCacheMaxSize(String maxSize) {
super.setCacheMaxSize(maxSize);
}
@Override
@Inject(value = StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_LRU_MODE, required = false)
protected void setUseLRUCache(String useLRUMode) {
super.setUseLRUCache(useLRUMode);
}
}
@@ -0,0 +1,61 @@
/*
* Copyright 2022 Apache Software Foundation.
*
* Licensed 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 com.opensymphony.xwork2.ognl;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.lang3.BooleanUtils;
/**
* Default OGNL Cache factory implementation.
*
* Currently used for Expression cache and BeanInfo cache creation.
*
* @param <Key> The type for the cache key entries
* @param <Value> The type for the cache value entries
*/
public class DefaultOgnlCacheFactory<Key, Value> implements OgnlCacheFactory {
private final AtomicBoolean useLRUCache = new AtomicBoolean(false);
private final AtomicInteger cacheMaxSize = new AtomicInteger(25000);
@Override
public OgnlCache<Key, Value> buildOgnlCache(int evictionLimit, int initialCapacity, float loadFactor, boolean lruCache) {
if (lruCache) {
return new OgnlLRUCache<>(evictionLimit, initialCapacity, loadFactor);
} else {
return new OgnlDefaultCache<>(evictionLimit, initialCapacity, loadFactor);
}
}
@Override
public int getCacheMaxSize() {
return cacheMaxSize.get();
}
protected void setCacheMaxSize(String maxSize) {
cacheMaxSize.set(Integer.parseInt(maxSize));
}
@Override
public boolean getUseLRUCache() {
return useLRUCache.get();
}
protected void setUseLRUCache(String useLRUMode) {
useLRUCache.set(BooleanUtils.toBoolean(useLRUMode));
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2022 Apache Software Foundation.
*
* Licensed 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 com.opensymphony.xwork2.ognl;
import com.opensymphony.xwork2.inject.Inject;
import org.apache.struts2.StrutsConstants;
/**
* Default OGNL Expression Cache factory implementation.
*
* Currently used for Expression cache creation.
*
* @param <Key> The type for the cache key entries
* @param <Value> The type for the cache value entries
*/
public class DefaultOgnlExpressionCacheFactory<Key, Value> extends DefaultOgnlCacheFactory {
@Override
@Inject(value = StrutsConstants.STRUTS_OGNL_EXPRESSION_CACHE_MAXSIZE, required = false)
protected void setCacheMaxSize(String maxSize) {
super.setCacheMaxSize(maxSize);
}
@Override
@Inject(value = StrutsConstants.STRUTS_OGNL_EXPRESSION_CACHE_LRU_MODE, required = false)
protected void setUseLRUCache(String useLRUMode) {
super.setUseLRUCache(useLRUMode);
}
}
@@ -0,0 +1,41 @@
/*
* Copyright 2022 Apache Software Foundation.
*
* Licensed 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 com.opensymphony.xwork2.ognl;
/**
* A basic cache interface for use with OGNL processing (such as Expression, BeanInfo).
* All OGNL caches will have an eviction limit, but setting an extremely high value can
* simulate an "effectively unlimited" cache.
*
* @param <Key> The type for the cache key entries
* @param <Value> The type for the cache value entries
*/
public interface OgnlCache<Key, Value> {
public Value get(Key key);
public void put(Key key, Value value);
public void putIfAbsent(Key key, Value value);
public int size();
public void clear();
public int getEvictionLimit();
public void setEvictionLimit(int cacheEvictionLimit);
}
@@ -0,0 +1,29 @@
/*
* Copyright 2022 Apache Software Foundation.
*
* Licensed 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 com.opensymphony.xwork2.ognl;
/**
* Used by {@link com.opensymphony.xwork2.ognl.OgnlUtil} to create appropriate OGNL
* caches based on configuration.
*
* @param <Key> The type for the cache key entries
* @param <Value> The type for the cache value entries
*/
public interface OgnlCacheFactory<Key, Value> {
OgnlCache<Key, Value> buildOgnlCache(int evictionLimit, int initialCapacity, float loadFactor, boolean lruCache);
int getCacheMaxSize();
boolean getUseLRUCache();
}
@@ -0,0 +1,85 @@
/*
* Copyright 2022 Apache Software Foundation.
*
* Licensed 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 com.opensymphony.xwork2.ognl;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Default OGNL cache implementation.
*
* Setting a very high eviction limit simulates an unlimited cache.
* Setting too low an eviction limit will make the cache ineffective.
*
* @param <Key> The type for the cache key entries
* @param <Value> The type for the cache value entries
*/
public class OgnlDefaultCache<Key, Value> implements OgnlCache<Key, Value> {
private final ConcurrentHashMap<Key, Value> ognlCache;
private final AtomicInteger cacheEvictionLimit = new AtomicInteger(25000);
public OgnlDefaultCache(int evictionLimit, int initialCapacity, float loadFactor) {
this.cacheEvictionLimit.set(evictionLimit);
ognlCache = new ConcurrentHashMap<>(initialCapacity, loadFactor);
}
@Override
public Value get(Key key) {
return ognlCache.get(key);
}
@Override
public void put(Key key, Value value) {
ognlCache.put(key, value);
this.clearIfEvictionLimitExceeded();
}
@Override
public void putIfAbsent(Key key, Value value) {
ognlCache.putIfAbsent(key, value);
this.clearIfEvictionLimitExceeded();
}
@Override
public int size() {
return ognlCache.size();
}
@Override
public void clear() {
ognlCache.clear();
}
@Override
public int getEvictionLimit() {
return this.cacheEvictionLimit.get();
}
@Override
public void setEvictionLimit(int cacheEvictionLimit) {
this.cacheEvictionLimit.set(cacheEvictionLimit);
}
/**
* Clear the cache if the eviction limit has been exceeded.
*/
private void clearIfEvictionLimitExceeded() {
if (ognlCache.size() > cacheEvictionLimit.get()) {
ognlCache.clear();
}
}
}
@@ -0,0 +1,87 @@
/*
* Copyright 2022 Apache Software Foundation.
*
* Licensed 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 com.opensymphony.xwork2.ognl;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* A basic OGNL LRU cache implementation.
*
* The implementation utilizes a {@link Collections#synchronizedMap(java.util.Map)}
* backed by a {@link LinkedHashMap}. May be replaced by a more efficient implementation in the future.
*
* Setting too low an eviction limit will produce more overhead than value.
* Setting too high an eviction limit may also produce more overhead than value.
* An appropriate eviction limit will need to be determined on an individual application basis.
*
* @param <Key> The type for the cache key entries
* @param <Value> The type for the cache value entries
*/
public class OgnlLRUCache<Key, Value> implements OgnlCache<Key, Value> {
private final Map<Key, Value> ognlLRUCache;
private final AtomicInteger cacheEvictionLimit = new AtomicInteger(2500);
public OgnlLRUCache(int evictionLimit, int initialCapacity, float loadFactor) {
this.cacheEvictionLimit.set(evictionLimit);
// Access-order mode selected (order mode true in LinkedHashMap constructor).
ognlLRUCache = Collections.synchronizedMap (new LinkedHashMap<Key, Value>(initialCapacity, loadFactor, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Key,Value> eldest) {
return (this.size() > cacheEvictionLimit.get());
}
});
}
@Override
public Value get(Key key) {
return ognlLRUCache.get(key);
}
@Override
public void put(Key key, Value value) {
ognlLRUCache.put(key, value);
}
@Override
public void putIfAbsent(Key key, Value value) {
ognlLRUCache.putIfAbsent(key, value);
}
@Override
public int size() {
return ognlLRUCache.size();
}
@Override
public void clear() {
ognlLRUCache.clear();
}
@Override
public int getEvictionLimit() {
return this.cacheEvictionLimit.get();
}
@Override
public void setEvictionLimit(int cacheEvictionLimit) {
this.cacheEvictionLimit.set(cacheEvictionLimit);
}
}
@@ -38,10 +38,7 @@ import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
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;
@@ -58,14 +55,10 @@ 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 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<String, Object> expressionsCache = new ConcurrentHashMap<>();
private final LRUCache<String, Object> expressionsCacheLRU = new LRUCache<>(expressionsCacheMaxSize.get(), 16, 0.75f);
private final ConcurrentMap<Class<?>, BeanInfo> beanInfoCache = new ConcurrentHashMap<>();
private final LRUCache<Class<?>, BeanInfo> beanInfoCacheLRU = new LRUCache<>(beanInfoCacheMaxSize.get(), 16, 0.75f);
private final OgnlCacheFactory<String, Object> ognlExpressionCacheFactory;
private final OgnlCacheFactory<Class<?>, BeanInfo> ognlBeanInfoCacheFactory;
private final OgnlCache<String, Object> expressionCache;
private final OgnlCache<Class<?>, BeanInfo> beanInfoCache;
private TypeConverter defaultConverter;
private boolean devMode;
@@ -85,7 +78,25 @@ public class OgnlUtil {
private boolean allowStaticMethodAccess;
private boolean disallowProxyMemberAccess;
/**
* Construct a new OgnlUtil instance for use with the framework
*/
public OgnlUtil() {
this(null, null); // Instantiate default Expression and BeanInfo caches (null factories)
}
/**
* Construct a new OgnlUtil instance for use with the framework, with optional
* cache factories for Ognl Expression and BeanInfo caches.
*
* @param ognlExpressionCacheFactory factory for Expression cache instance. If null, use default
* @param ognlBeanInfoCacheFactory for BeanInfo cache instance. If null, use default
*/
@Inject
public OgnlUtil(
@Inject(value = StrutsConstants.STRUTS_OGNL_EXPRESSIONCACHE_FACTORY, required = false) OgnlCacheFactory<String, Object> ognlExpressionCacheFactory,
@Inject(value = StrutsConstants.STRUTS_OGNL_BEANINFOCACHE_FACTORY, required = false) OgnlCacheFactory<Class<?>, BeanInfo> ognlBeanInfoCacheFactory
) {
excludedClasses = Collections.unmodifiableSet(new HashSet<>());
excludedPackageNamePatterns = Collections.unmodifiableSet(new HashSet<>());
excludedPackageNames = Collections.unmodifiableSet(new HashSet<>());
@@ -93,6 +104,19 @@ public class OgnlUtil {
devModeExcludedClasses = Collections.unmodifiableSet(new HashSet<>());
devModeExcludedPackageNamePatterns = Collections.unmodifiableSet(new HashSet<>());
devModeExcludedPackageNames = Collections.unmodifiableSet(new HashSet<>());
this.ognlExpressionCacheFactory = ognlExpressionCacheFactory;
this.ognlBeanInfoCacheFactory = ognlBeanInfoCacheFactory;
if (ognlExpressionCacheFactory != null) {
this.expressionCache = ognlExpressionCacheFactory.buildOgnlCache(ognlExpressionCacheFactory.getCacheMaxSize(), 16, 0.75f, ognlExpressionCacheFactory.getUseLRUCache());
} else {
this.expressionCache = new OgnlDefaultCache<>(25000, 16, 0.75f);
}
if (ognlBeanInfoCacheFactory != null) {
this.beanInfoCache = ognlBeanInfoCacheFactory.buildOgnlCache(ognlBeanInfoCacheFactory.getCacheMaxSize(), 16, 0.75f, ognlBeanInfoCacheFactory.getUseLRUCache());
} else {
this.beanInfoCache = new OgnlDefaultCache<>(25000, 16, 0.75f);
}
}
@Inject
@@ -111,25 +135,13 @@ public class OgnlUtil {
}
@Inject(value = StrutsConstants.STRUTS_OGNL_EXPRESSION_CACHE_MAXSIZE, required = false)
protected void setExpressionsCacheMaxSize(String maxSize) {
expressionsCacheMaxSize.set(Integer.parseInt(maxSize));
expressionsCacheLRU.setEvictionLimit(expressionsCacheMaxSize.get());
protected void setExpressionCacheMaxSize(String maxSize) {
expressionCache.setEvictionLimit(Integer.parseInt(maxSize));
}
@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));
beanInfoCache.setEvictionLimit(Integer.parseInt(maxSize));
}
@Inject(value = StrutsConstants.STRUTS_OGNL_ENABLE_EVAL_EXPRESSION, required = false)
@@ -302,8 +314,7 @@ public class OgnlUtil {
* @since 2.5.21
*/
public void clearExpressionCache() {
expressionsCache.clear();
expressionsCacheLRU.clear();
expressionCache.clear();
}
/**
@@ -314,11 +325,7 @@ public class OgnlUtil {
* @since 2.5.21
*/
public int expressionCacheSize() {
if (useLRUExpressionCache.get()) {
return expressionsCacheLRU.size();
} else {
return expressionsCache.size();
}
return expressionCache.size();
}
/**
@@ -336,7 +343,6 @@ public class OgnlUtil {
*/
public void clearBeanInfoCache() {
beanInfoCache.clear();
beanInfoCacheLRU.clear();
}
/**
@@ -347,11 +353,7 @@ public class OgnlUtil {
* @since 2.5.21
*/
public int beanInfoCacheSize() {
if (useLRUBeanInfoCache.get()) {
return beanInfoCacheLRU.size();
} else {
return beanInfoCache.size();
}
return beanInfoCache.size();
}
/**
@@ -571,18 +573,11 @@ public class OgnlUtil {
private <T> Object compileAndExecute(String expression, Map<String, Object> context, OgnlTask<T> task) throws OgnlException {
Object tree;
if (enableExpressionCache) {
final Map<String, Object> expressionsCacheRef = (useLRUExpressionCache.get() ? expressionsCacheLRU.backingMapReference() : expressionsCache);
tree = expressionsCacheRef.get(expression);
tree = expressionCache.get(expression);
if (tree == null) {
tree = Ognl.parseExpression(expression);
checkEnableEvalExpression(tree, context);
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.
}
}
expressionCache.putIfAbsent(expression, tree);
}
} else {
tree = Ognl.parseExpression(expression);
@@ -595,18 +590,11 @@ public class OgnlUtil {
private <T> Object compileAndExecuteMethod(String expression, Map<String, Object> context, OgnlTask<T> task) throws OgnlException {
Object tree;
if (enableExpressionCache) {
final Map<String, Object> expressionsCacheRef = (useLRUExpressionCache.get() ? expressionsCacheLRU.backingMapReference() : expressionsCache);
tree = expressionsCacheRef.get(expression);
tree = expressionCache.get(expression);
if (tree == null) {
tree = Ognl.parseExpression(expression);
checkSimpleMethod(tree, context);
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.
}
}
expressionCache.putIfAbsent(expression, tree);
}
} else {
tree = Ognl.parseExpression(expression);
@@ -812,18 +800,11 @@ public class OgnlUtil {
* @throws IntrospectionException is thrown if an exception occurs during introspection.
*/
public BeanInfo getBeanInfo(Class<?> clazz) throws IntrospectionException {
final Map<Class<?>, BeanInfo> beanInfoCacheRef = (useLRUBeanInfoCache.get() ? beanInfoCacheLRU.backingMapReference() : beanInfoCache);
synchronized (beanInfoCacheRef) {
BeanInfo beanInfo = beanInfoCacheRef.get(clazz);
synchronized (beanInfoCache) {
BeanInfo beanInfo = beanInfoCache.get(clazz);
if (beanInfo == null) {
beanInfo = Introspector.getBeanInfo(clazz, Object.class);
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.
}
}
beanInfoCache.putIfAbsent(clazz, beanInfo);
}
return beanInfo;
}
@@ -882,66 +863,4 @@ 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> Key type for the LRUCache
* @param <Value> Value type for the LRUCache
*/
protected class LRUCache<Key, Value> {
private final Map<Key, Value> 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<Key, Value>(initialCapacity, loadFactor, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Key,Value> 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<Key, Value> backingMapReference() {
return lruCache;
}
public int getEvictionLimit() {
return this.cacheEvictionLimit.get();
}
public void setEvictionLimit(int cacheEvictionLimit) {
this.cacheEvictionLimit.set(cacheEvictionLimit);
}
}
}
@@ -249,6 +249,18 @@ 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 an OGNL expression cache factory implementation. A default implementation is provided, but
* could be replaced by a custom one if desired.
*/
public static final String STRUTS_OGNL_EXPRESSIONCACHE_FACTORY = "struts.ognl.expressionCacheFactory";
/**
* Specifies an OGNL BeanInfo cache factory implementation. A default implementation is provided, but
* could be replaced by a custom one if desired.
*/
public static final String STRUTS_OGNL_BEANINFOCACHE_FACTORY = "struts.ognl.beanInfoCacheFactory";
/**
* 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
@@ -230,12 +230,12 @@ struts.ognl.enableExpressionCache=true
### 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
# 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
# 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
@@ -243,12 +243,12 @@ struts.ognl.enableExpressionCache=true
### 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
# 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
### 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
@@ -228,6 +228,9 @@
<bean type="com.opensymphony.xwork2.config.providers.ValueSubstitutor" class="com.opensymphony.xwork2.config.providers.EnvsValueSubstitutor" scope="singleton"/>
<bean type="com.opensymphony.xwork2.ognl.OgnlCacheFactory" name="struts.ognl.expressionCacheFactory" class="com.opensymphony.xwork2.ognl.DefaultOgnlExpressionCacheFactory" scope="singleton"/>
<bean type="com.opensymphony.xwork2.ognl.OgnlCacheFactory" name="struts.ognl.beanInfoCacheFactory" class="com.opensymphony.xwork2.ognl.DefaultOgnlBeanInfoCacheFactory" scope="singleton"/>
<package name="struts-default" abstract="true">
<result-types>
<result-type name="chain" class="com.opensymphony.xwork2.ActionChainResult"/>
@@ -35,6 +35,7 @@ import com.opensymphony.xwork2.util.Owner;
import com.opensymphony.xwork2.util.ValueStack;
import com.opensymphony.xwork2.util.location.LocatableProperties;
import com.opensymphony.xwork2.util.reflection.ReflectionContextState;
import java.beans.BeanInfo;
import ognl.InappropriateExpressionException;
import ognl.MethodFailedException;
import ognl.NoSuchPropertyException;
@@ -131,7 +132,7 @@ public class OgnlUtilTest extends XWorkTestCase {
public void testCacheEnabledMaxSize() throws OgnlException {
ognlUtil.setEnableExpressionCache("true");
ognlUtil.setExpressionsCacheMaxSize("1");
ognlUtil.setExpressionCacheMaxSize("1");
Object expr0 = ognlUtil.compile("test");
Object expr2 = ognlUtil.compile("test");
assertSame(expr0, expr2);
@@ -153,22 +154,22 @@ public class OgnlUtilTest extends XWorkTestCase {
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 {
// Force usage of LRU cache factories for the OgnlUtil instance
this.ognlUtil = generateOgnlUtilInstanceWithDefaultLRUCacheFactories();
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 {
// Force usage of LRU cache factories for the OgnlUtil instance
this.ognlUtil = generateOgnlUtilInstanceWithDefaultLRUCacheFactories();
ognlUtil.setEnableExpressionCache("true");
ognlUtil.setUseLRUExpressionCache("true");
ognlUtil.setExpressionsCacheMaxSize("1");
ognlUtil.setExpressionCacheMaxSize("1");
Object expr0 = ognlUtil.compile("test");
Object expr2 = ognlUtil.compile("test");
assertSame(expr0, expr2);
@@ -182,7 +183,6 @@ public class OgnlUtilTest extends XWorkTestCase {
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 {
@@ -206,8 +206,10 @@ public class OgnlUtilTest extends XWorkTestCase {
}
public void testExpressionIsLRUCachedIrrespectiveOfItsExecutionStatus() throws OgnlException {
// Force usage of LRU cache factories for the OgnlUtil instance
this.ognlUtil = generateOgnlUtilInstanceWithDefaultLRUCacheFactories();
ognlUtil.setContainer(container); // Must be explicitly set as the generated OgnlUtil instance has no container
ognlUtil.setEnableExpressionCache("true");
ognlUtil.setUseLRUExpressionCache("true");
Foo foo = new Foo();
OgnlContext context = (OgnlContext) ognlUtil.createDefaultContext(foo);
@@ -225,7 +227,6 @@ public class OgnlUtilTest extends XWorkTestCase {
} catch (Exception ex) {
assertEquals("Expression with failed execution must have been cached nevertheless", ognlUtil.expressionCacheSize(), 2);
}
ognlUtil.setUseLRUExpressionCache("false");
}
public void testMethodExpressionIsCachedIrrespectiveOfItsExecutionStatus() throws Exception {
@@ -273,8 +274,9 @@ public class OgnlUtilTest extends XWorkTestCase {
}
public void testClearExpressionLRUCache() throws OgnlException {
// Force usage of LRU cache factories for the OgnlUtil instance
this.ognlUtil = generateOgnlUtilInstanceWithDefaultLRUCacheFactories();
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");
@@ -295,7 +297,6 @@ public class OgnlUtilTest extends XWorkTestCase {
assertSame(expr3, expr4);
assertSame(expr3, expr5);
assertTrue("Expression cache empty after usage ?", ognlUtil.expressionCacheSize() > 0);
ognlUtil.setUseLRUExpressionCache("false");
}
public void testClearBeanInfoCache() throws IntrospectionException {
@@ -369,7 +370,8 @@ public class OgnlUtilTest extends XWorkTestCase {
}
public void testBeanInfoLRUCache() throws IntrospectionException {
ognlUtil.setUseLRUBeanInfoCache("true");
// Force usage of LRU cache factories for the OgnlUtil instance
this.ognlUtil = generateOgnlUtilInstanceWithDefaultLRUCacheFactories();
final TestBean1 testBean1 = new TestBean1();
final TestBean2 testBean2 = new TestBean2();
// Test that the BeanInfo cache is functioning as expected.
@@ -388,11 +390,11 @@ public class OgnlUtilTest extends XWorkTestCase {
// 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");
// Force usage of LRU cache factories for the OgnlUtil instance
this.ognlUtil = generateOgnlUtilInstanceWithDefaultLRUCacheFactories();
ognlUtil.setBeanInfoCacheMaxSize("1");
final TestBean1 testBean1 = new TestBean1();
final TestBean2 testBean2 = new TestBean2();
@@ -416,7 +418,6 @@ public class OgnlUtilTest extends XWorkTestCase {
// 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));
}
@@ -1312,6 +1313,20 @@ public class OgnlUtilTest extends XWorkTestCase {
internalTestOgnlUtilExclusionsImmutable(basicOgnlUtil);
}
public void testDefaultOgnlUtilExclusionsAlternateConstructor() {
OgnlUtil basicOgnlUtil = new OgnlUtil(null, null);
internalTestInitialEmptyOgnlUtilExclusions(basicOgnlUtil);
internalTestOgnlUtilExclusionsImmutable(basicOgnlUtil);
}
public void testDefaultOgnlUtilExclusionsAlternateConstructorPopulated() {
OgnlUtil basicOgnlUtil = new OgnlUtil(new DefaultOgnlExpressionCacheFactory<String, Object>(), new DefaultOgnlBeanInfoCacheFactory<Class<?>, BeanInfo>());
internalTestInitialEmptyOgnlUtilExclusions(basicOgnlUtil);
internalTestOgnlUtilExclusionsImmutable(basicOgnlUtil);
}
public void testOgnlUtilExcludedAdditivity() {
Set<Class<?>> excludedClasses;
Set<Pattern> excludedPackageNamePatterns;
@@ -1675,6 +1690,34 @@ public class OgnlUtilTest extends XWorkTestCase {
}
}
public void testGetExcludedPackageNamesAlternateConstructor() {
// Getter should return an immutable collection
OgnlUtil util = new OgnlUtil(null, null);
util.setExcludedPackageNames("java.lang,java.awt");
assertEquals(util.getExcludedPackageNames().size(), 2);
try {
util.getExcludedPackageNames().clear();
} catch (Exception ex) {
assertTrue(ex instanceof UnsupportedOperationException);
} finally {
assertEquals(util.getExcludedPackageNames().size(), 2);
}
}
public void testGetExcludedPackageNamesAlternateConstructorPopulated() {
// Getter should return an immutable collection
OgnlUtil util = new OgnlUtil(new DefaultOgnlExpressionCacheFactory<String, Object>(), new DefaultOgnlBeanInfoCacheFactory<Class<?>, BeanInfo>());
util.setExcludedPackageNames("java.lang,java.awt");
assertEquals(util.getExcludedPackageNames().size(), 2);
try {
util.getExcludedPackageNames().clear();
} catch (Exception ex) {
assertTrue(ex instanceof UnsupportedOperationException);
} finally {
assertEquals(util.getExcludedPackageNames().size(), 2);
}
}
public void testGetExcludedClasses() {
// Getter should return an immutable collection
OgnlUtil util = new OgnlUtil();
@@ -1689,6 +1732,34 @@ public class OgnlUtilTest extends XWorkTestCase {
}
}
public void testGetExcludedClassesAlternateConstructor() {
// Getter should return an immutable collection
OgnlUtil util = new OgnlUtil(null, null);
util.setExcludedClasses("java.lang.Runtime,java.lang.ProcessBuilder,java.net.URL");
assertEquals(util.getExcludedClasses().size(), 3);
try {
util.getExcludedClasses().clear();
} catch (Exception ex) {
assertTrue(ex instanceof UnsupportedOperationException);
} finally {
assertEquals(util.getExcludedClasses().size(), 3);
}
}
public void testGetExcludedClassesAlternateConstructorPopulated() {
// Getter should return an immutable collection
OgnlUtil util = new OgnlUtil(new DefaultOgnlExpressionCacheFactory<String, Object>(), new DefaultOgnlBeanInfoCacheFactory<Class<?>, BeanInfo>());
util.setExcludedClasses("java.lang.Runtime,java.lang.ProcessBuilder,java.net.URL");
assertEquals(util.getExcludedClasses().size(), 3);
try {
util.getExcludedClasses().clear();
} catch (Exception ex) {
assertTrue(ex instanceof UnsupportedOperationException);
} finally {
assertEquals(util.getExcludedClasses().size(), 3);
}
}
public void testGetExcludedPackageNamePatterns() {
// Getter should return an immutable collection
OgnlUtil util = new OgnlUtil();
@@ -1703,10 +1774,64 @@ public class OgnlUtilTest extends XWorkTestCase {
}
}
public void testGetExcludedPackageNamePatternsAlternateConstructor() {
// Getter should return an immutable collection
OgnlUtil util = new OgnlUtil(null, null);
util.setExcludedPackageNamePatterns("java.lang.");
assertEquals(util.getExcludedPackageNamePatterns().size(), 1);
try {
util.getExcludedPackageNamePatterns().clear();
} catch (Exception ex) {
assertTrue(ex instanceof UnsupportedOperationException);
} finally {
assertEquals(util.getExcludedPackageNamePatterns().size(), 1);
}
}
public void testGetExcludedPackageNamePatternsAlternateConstructorPopulated() {
// Getter should return an immutable collection
OgnlUtil util = new OgnlUtil(new DefaultOgnlExpressionCacheFactory<String, Object>(), new DefaultOgnlBeanInfoCacheFactory<Class<?>, BeanInfo>());
util.setExcludedPackageNamePatterns("java.lang.");
assertEquals(util.getExcludedPackageNamePatterns().size(), 1);
try {
util.getExcludedPackageNamePatterns().clear();
} catch (Exception ex) {
assertTrue(ex instanceof UnsupportedOperationException);
} finally {
assertEquals(util.getExcludedPackageNamePatterns().size(), 1);
}
}
public void testOgnlUtilDefaultCacheClass() throws OgnlException {
OgnlDefaultCache<Integer, String> defaultCache = new OgnlDefaultCache<>(2, 16, 0.75f);
assertEquals("Initial evictionLimit did not match initial value", 2, defaultCache.getEvictionLimit());
defaultCache.setEvictionLimit(3);
assertEquals("Updated evictionLimit did not match updated value", 3, defaultCache.getEvictionLimit());
String lookupResult = defaultCache.get(Integer.valueOf(0));
assertNull("Lookup of empty cache returned non-null value ?", lookupResult);
defaultCache.put(Integer.valueOf(0), "Zero");
lookupResult = defaultCache.get(Integer.valueOf(0));
assertEquals("Retrieved value does not match put value ?", "Zero", lookupResult);
defaultCache.put(Integer.valueOf(1), "One");
defaultCache.put(Integer.valueOf(2), "Two");
assertEquals("Default cache not size evictionlimit after adding three values ?", defaultCache.getEvictionLimit(), defaultCache.size());
lookupResult = defaultCache.get(Integer.valueOf(2));
assertEquals("Retrieved value does not match put value ?", "Two", lookupResult);
defaultCache.put(Integer.valueOf(3), "Three");
assertEquals("Default cache not size zero after an add that exceeded the evection limit ?", 0, defaultCache.size());
lookupResult = defaultCache.get(Integer.valueOf(0));
assertNull("Lookup of value 0 (should have been evicted with everything) returned non-null value ?", lookupResult);
lookupResult = defaultCache.get(Integer.valueOf(3));
assertNull("Lookup of value 3 (should have been evicted with everything) returned non-null value ?", lookupResult);
defaultCache.putIfAbsent(Integer.valueOf(2), "Two");
lookupResult = defaultCache.get(Integer.valueOf(2));
assertEquals("Retrieved value does not match put value ?", "Two", lookupResult);
defaultCache.clear();
assertEquals("Default cache not empty after clear ?", 0, defaultCache.size());
}
public void testOgnlUtilLRUCacheClass() throws OgnlException {
OgnlUtil.LRUCache<Integer, String> lruCache = ognlUtil.new LRUCache<>(2, 16, 0.75f);
Map<Integer, String> backingMap = lruCache.backingMapReference();
assertNotNull("Backing Map somehow null ?", backingMap);
OgnlLRUCache<Integer, String> lruCache = new OgnlLRUCache<>(2, 16, 0.75f);
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());
@@ -1727,10 +1852,24 @@ public class OgnlUtilTest extends XWorkTestCase {
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());
}
/**
* Generate a new OgnlUtil instance (not configured by the {@link ContainerBuilder}) that can be used for
* basic tests, with its Expression and BeanInfo factories set to LRU mode.
*
* @return OgnlUtil instance with LRU enabled Expression and BeanInfo factories
*/
private OgnlUtil generateOgnlUtilInstanceWithDefaultLRUCacheFactories() {
final OgnlUtil result;
final DefaultOgnlCacheFactory expressionFactory = new DefaultOgnlExpressionCacheFactory<String, Object>();
final DefaultOgnlCacheFactory beanInfoFactory = new DefaultOgnlBeanInfoCacheFactory<Class<?>, BeanInfo>();
expressionFactory.setUseLRUCache("true");
beanInfoFactory.setUseLRUCache("true");
result = new OgnlUtil(expressionFactory, beanInfoFactory);
return result;
}
private void reloadTestContainerConfiguration(boolean devMode, boolean allowStaticMethod) {