WW-5514: Make ProxyUtil cache configurable via struts constants (#1573)

* fix(ognl): make ProxyUtil cache configurable via struts constants

Makes the ProxyUtil cache type configurable through Struts constants,
allowing applications to use BASIC cache type (default) without
requiring Caffeine as a mandatory dependency.

New configuration properties:
- struts.proxy.cacheType: basic (default), lru, or wtlfu
- struts.proxy.cacheMaxSize: 10000 (default)

Fixes WW-5514

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(ognl): use LazyRef for proxy caches and reset on factory change

Extract lazy initialization into reusable LazyRef<T> utility with
double-checked locking and reset support. ProxyUtil.setProxyCacheFactory()
now resets both caches so they are recreated with the new factory,
fixing the bug where caches were never refreshed after factory changes.
Default proxy cache type changed from 'basic' to 'wtlfu' for consistency
with expression and beanInfo caches. Fix @since version to 6.9.0.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Lukasz Lenart
2026-02-21 18:17:39 +01:00
committed by GitHub
parent 2bd1b60802
commit 18d6e77bf2
9 changed files with 265 additions and 11 deletions
@@ -82,6 +82,8 @@ import com.opensymphony.xwork2.ognl.BeanInfoCacheFactory;
import com.opensymphony.xwork2.ognl.DefaultOgnlBeanInfoCacheFactory;
import com.opensymphony.xwork2.ognl.DefaultOgnlExpressionCacheFactory;
import com.opensymphony.xwork2.ognl.ExpressionCacheFactory;
import com.opensymphony.xwork2.ognl.ProxyCacheFactory;
import com.opensymphony.xwork2.ognl.StrutsProxyCacheFactory;
import com.opensymphony.xwork2.ognl.OgnlCacheFactory;
import com.opensymphony.xwork2.ognl.OgnlReflectionProvider;
import com.opensymphony.xwork2.ognl.OgnlUtil;
@@ -93,6 +95,7 @@ import com.opensymphony.xwork2.ognl.accessor.XWorkMethodAccessor;
import com.opensymphony.xwork2.util.OgnlTextParser;
import com.opensymphony.xwork2.util.PatternMatcher;
import com.opensymphony.xwork2.util.StrutsLocalizedTextProvider;
import com.opensymphony.xwork2.util.StrutsProxyCacheFactoryBean;
import com.opensymphony.xwork2.util.TextParser;
import com.opensymphony.xwork2.util.ValueStack;
import com.opensymphony.xwork2.util.ValueStackFactory;
@@ -143,6 +146,8 @@ public class DefaultConfiguration implements Configuration {
constants.put(StrutsConstants.STRUTS_OGNL_EXPRESSION_CACHE_MAXSIZE, 10000);
constants.put(StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_TYPE, OgnlCacheFactory.CacheType.BASIC);
constants.put(StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_MAXSIZE, 10000);
constants.put(StrutsConstants.STRUTS_PROXY_CACHE_TYPE, OgnlCacheFactory.CacheType.BASIC);
constants.put(StrutsConstants.STRUTS_PROXY_CACHE_MAXSIZE, 10000);
constants.put(StrutsConstants.STRUTS_ENABLE_DYNAMIC_METHOD_INVOCATION, Boolean.FALSE);
BOOTSTRAP_CONSTANTS = Collections.unmodifiableMap(constants);
}
@@ -394,6 +399,8 @@ public class DefaultConfiguration implements Configuration {
.factory(ExpressionCacheFactory.class, DefaultOgnlExpressionCacheFactory.class, Scope.SINGLETON)
.factory(BeanInfoCacheFactory.class, DefaultOgnlBeanInfoCacheFactory.class, Scope.SINGLETON)
.factory(ProxyCacheFactory.class, StrutsProxyCacheFactory.class, Scope.SINGLETON)
.factory(StrutsProxyCacheFactoryBean.class, Scope.SINGLETON)
.factory(OgnlUtil.class, Scope.SINGLETON)
.factory(SecurityMemberAccess.class, Scope.PROTOTYPE)
.factory(OgnlGuard.class, StrutsOgnlGuard.class, Scope.SINGLETON)
@@ -0,0 +1,31 @@
/*
* 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 com.opensymphony.xwork2.ognl;
/**
* A factory interface for ProxyUtil cache to be used with Struts DI mechanism.
* This allows the proxy detection cache type to be configurable via Struts constants.
*
* @param <Key> The type for the cache key entries
* @param <Value> The type for the cache value entries
* @since 6.9.0
*/
public interface ProxyCacheFactory<Key, Value> extends OgnlCacheFactory<Key, Value> {
}
@@ -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 com.opensymphony.xwork2.ognl;
import com.opensymphony.xwork2.inject.Inject;
import org.apache.commons.lang3.EnumUtils;
import org.apache.struts2.StrutsConstants;
/**
* Struts Proxy Cache factory implementation for ProxyUtil caches.
* <p>
* This factory is used to create caches for proxy detection in ProxyUtil.
* The cache type and size can be configured via Struts constants.
*
* @param <Key> The type for the cache key entries
* @param <Value> The type for the cache value entries
* @since 6.9.0
*/
public class StrutsProxyCacheFactory<Key, Value> extends DefaultOgnlCacheFactory<Key, Value>
implements ProxyCacheFactory<Key, Value> {
@Inject
public StrutsProxyCacheFactory(
@Inject(value = StrutsConstants.STRUTS_PROXY_CACHE_MAXSIZE) String cacheMaxSize,
@Inject(value = StrutsConstants.STRUTS_PROXY_CACHE_TYPE) String defaultCacheType) {
super(Integer.parseInt(cacheMaxSize), EnumUtils.getEnumIgnoreCase(CacheType.class, defaultCacheType));
}
}
@@ -0,0 +1,71 @@
/*
* 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 com.opensymphony.xwork2.util;
import java.util.function.Supplier;
/**
* A thread-safe lazy reference that computes its value on first access using
* double-checked locking. The cached value can be invalidated via {@link #reset()},
* causing the next {@link #get()} call to recompute the value.
*
* @param <T> the type of the lazily computed value
* @since 6.9.0
*/
public class LazyRef<T> implements Supplier<T> {
private final Supplier<T> factory;
private volatile T value;
/**
* Creates a new LazyRef with the given factory supplier.
*
* @param factory the supplier used to compute the value; must not be null
*/
public LazyRef(Supplier<T> factory) {
this.factory = factory;
}
/**
* Returns the cached value, computing it on first access or after a {@link #reset()}.
*
* @return the computed value
*/
@Override
public T get() {
T result = value;
if (result == null) {
synchronized (this) {
result = value;
if (result == null) {
result = factory.get();
value = result;
}
}
}
return result;
}
/**
* Invalidates the cached value so the next {@link #get()} call recomputes it.
*/
public void reset() {
value = null;
}
}
@@ -21,6 +21,7 @@ package com.opensymphony.xwork2.util;
import com.opensymphony.xwork2.ognl.DefaultOgnlCacheFactory;
import com.opensymphony.xwork2.ognl.OgnlCache;
import com.opensymphony.xwork2.ognl.OgnlCacheFactory;
import com.opensymphony.xwork2.ognl.ProxyCacheFactory;
import org.apache.commons.lang3.reflect.ConstructorUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
@@ -41,7 +42,6 @@ import static java.lang.reflect.Modifier.isPublic;
* <p>
* Various utility methods dealing with proxies
* </p>
*
*/
public class ProxyUtil {
private static final String SPRING_ADVISED_CLASS_NAME = "org.springframework.aop.framework.Advised";
@@ -51,15 +51,45 @@ public class ProxyUtil {
private static final String HIBERNATE_HIBERNATEPROXY_CLASS_NAME = "org.hibernate.proxy.HibernateProxy";
private static final int CACHE_MAX_SIZE = 10000;
private static final int CACHE_INITIAL_CAPACITY = 256;
private static final OgnlCache<Class<?>, Boolean> isProxyCache = new DefaultOgnlCacheFactory<Class<?>, Boolean>(
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache();
private static final OgnlCache<Member, Boolean> isProxyMemberCache = new DefaultOgnlCacheFactory<Member, Boolean>(
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache();
// Holder for the cache factory (set by container)
private static volatile ProxyCacheFactory<?, ?> cacheFactory;
// Lazy-initialized caches with reset support
private static final LazyRef<OgnlCache<Class<?>, Boolean>> isProxyCache =
new LazyRef<>(ProxyUtil::createCache);
private static final LazyRef<OgnlCache<Member, Boolean>> isProxyMemberCache =
new LazyRef<>(ProxyUtil::createCache);
/**
* Sets the cache factory. Called by the container during initialization.
* Resets existing caches so they are recreated with the new factory.
*
* @param factory the cache factory to use for creating proxy caches
* @since 6.9.0
*/
public static void setProxyCacheFactory(ProxyCacheFactory<?, ?> factory) {
cacheFactory = factory;
isProxyCache.reset();
isProxyMemberCache.reset();
}
@SuppressWarnings("unchecked")
private static <K, V> OgnlCache<K, V> createCache() {
if (cacheFactory != null) {
return ((ProxyCacheFactory<K, V>) cacheFactory).buildOgnlCache(
CACHE_MAX_SIZE, CACHE_INITIAL_CAPACITY, 0.75f, cacheFactory.getDefaultCacheType());
}
// Fallback to BASIC if container hasn't initialized yet
return new DefaultOgnlCacheFactory<K, V>(
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.BASIC, CACHE_INITIAL_CAPACITY).buildOgnlCache();
}
/**
* Determine the ultimate target class of the given instance, traversing
* not only a top-level proxy but any number of nested proxies as well &mdash;
* as long as possible without side effects.
*
* @param candidate the instance to check (might be a proxy)
* @return the ultimate target class (or the plain class of the given
* object as fallback; never {@code null})
@@ -78,24 +108,26 @@ public class ProxyUtil {
/**
* Check whether the given object is a proxy.
*
* @param object the object to check
*/
public static boolean isProxy(Object object) {
if (object == null) return false;
Class<?> clazz = object.getClass();
Boolean flag = isProxyCache.get(clazz);
Boolean flag = isProxyCache.get().get(clazz);
if (flag != null) {
return flag;
}
boolean isProxy = isSpringAopProxy(object) || isHibernateProxy(object);
isProxyCache.put(clazz, isProxy);
isProxyCache.get().put(clazz, isProxy);
return isProxy;
}
/**
* Check whether the given member is a proxy member of a proxy object or is a static proxy member.
*
* @param member the member to check
* @param object the object to check
*/
@@ -104,14 +136,14 @@ public class ProxyUtil {
return false;
}
Boolean flag = isProxyMemberCache.get(member);
Boolean flag = isProxyMemberCache.get().get(member);
if (flag != null) {
return flag;
}
boolean isProxyMember = isSpringProxyMember(member) || isHibernateProxyMember(member);
isProxyMemberCache.put(member, isProxyMember);
isProxyMemberCache.get().put(member, isProxyMember);
return isProxyMember;
}
@@ -147,6 +179,7 @@ public class ProxyUtil {
* Determine the ultimate target class of the given spring bean instance, traversing
* not only a top-level spring proxy but any number of nested spring proxies as well &mdash;
* as long as possible without side effects, that is, just for singleton targets.
*
* @param candidate the instance to check (might be a spring AOP proxy)
* @return the ultimate target class (or the plain class of the given
* object as fallback; never {@code null})
@@ -170,6 +203,7 @@ public class ProxyUtil {
/**
* Check whether the given object is a Spring proxy.
*
* @param object the object to check
*/
private static boolean isSpringAopProxy(Object object) {
@@ -180,6 +214,7 @@ public class ProxyUtil {
/**
* Check whether the given member is a member of a spring proxy.
*
* @param member the member to check
*/
private static boolean isSpringProxyMember(Member member) {
@@ -201,6 +236,7 @@ public class ProxyUtil {
/**
* Obtain the singleton target object behind the given spring proxy, if any.
*
* @param candidate the (potential) spring proxy to check
* @return the singleton target object, or {@code null} in any other case
* (not a spring proxy, not an existing singleton target)
@@ -221,6 +257,7 @@ public class ProxyUtil {
/**
* Check whether the specified class is a CGLIB-generated class.
*
* @param clazz the class to check
*/
private static boolean isCglibProxyClass(Class<?> clazz) {
@@ -229,7 +266,8 @@ public class ProxyUtil {
/**
* Check whether the given class implements an interface with a given class name.
* @param clazz the class to check
*
* @param clazz the class to check
* @param ifaceClassName the interface class name to check
*/
private static boolean implementsInterface(Class<?> clazz, String ifaceClassName) {
@@ -243,7 +281,8 @@ public class ProxyUtil {
/**
* Check whether the given class has a given member.
* @param clazz the class to check
*
* @param clazz the class to check
* @param member the member to check
*/
private static boolean hasMember(Class<?> clazz, Member member) {
@@ -0,0 +1,38 @@
/*
* 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 com.opensymphony.xwork2.util;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.ognl.ProxyCacheFactory;
/**
* Bean that wires the ProxyCacheFactory to ProxyUtil during container initialization.
* <p>
* This bean is created by the container and receives the configured ProxyCacheFactory
* via dependency injection, then passes it to the static ProxyUtil class.
*
* @since 6.9.0
*/
public class StrutsProxyCacheFactoryBean {
@Inject
public StrutsProxyCacheFactoryBean(ProxyCacheFactory<?, ?> proxyCacheFactory) {
ProxyUtil.setProxyCacheFactory(proxyCacheFactory);
}
}
@@ -517,4 +517,18 @@ public final class StrutsConstants {
*/
public static final String STRUTS_CSP_NONCE_READER = "struts.csp.nonce.reader";
public static final String STRUTS_CSP_NONCE_SOURCE = "struts.csp.nonce.source";
/**
* Specifies the type of cache to use for proxy detection in ProxyUtil.
* Valid values defined in {@link com.opensymphony.xwork2.ognl.OgnlCacheFactory.CacheType}.
* Default is 'basic' (no Caffeine dependency required).
* @since 6.8.0
*/
public static final String STRUTS_PROXY_CACHE_TYPE = "struts.proxy.cacheType";
/**
* Specifies the maximum cache size for proxy detection caches in ProxyUtil.
* @since 6.8.0
*/
public static final String STRUTS_PROXY_CACHE_MAXSIZE = "struts.proxy.cacheMaxSize";
}
@@ -247,6 +247,13 @@ struts.ognl.beanInfoCacheType=wtlfu
### application-specific needs.
struts.ognl.beanInfoCacheMaxSize=10000
### Specifies the type of cache to use for proxy detection in ProxyUtil.
### Valid values: basic, lru, wtlfu. Default is 'wtlfu'.
struts.proxy.cacheType=wtlfu
### Specifies the maximum cache size for proxy detection caches.
struts.proxy.cacheMaxSize=10000
### 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
+3
View File
@@ -255,6 +255,9 @@
class="com.opensymphony.xwork2.ognl.DefaultOgnlExpressionCacheFactory" scope="singleton"/>
<bean type="com.opensymphony.xwork2.ognl.BeanInfoCacheFactory" name="struts"
class="com.opensymphony.xwork2.ognl.DefaultOgnlBeanInfoCacheFactory" scope="singleton"/>
<bean type="com.opensymphony.xwork2.ognl.ProxyCacheFactory" name="struts"
class="com.opensymphony.xwork2.ognl.StrutsProxyCacheFactory" scope="singleton"/>
<bean class="com.opensymphony.xwork2.util.StrutsProxyCacheFactoryBean" scope="singleton"/>
<bean type="org.apache.struts2.url.QueryStringBuilder" name="strutsQueryStringBuilder"
class="org.apache.struts2.url.StrutsQueryStringBuilder" scope="singleton"/>