This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch fix/WW-5514-proxy-cache-configurable
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 2b924d92385f0c567a61028123521b8618d5dcb0
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon Feb 9 19:02:29 2026 +0200

    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 <[email protected]>
---
 .../xwork2/config/impl/DefaultConfiguration.java   | 47 +++++++------
 .../xwork2/ognl/ProxyCacheFactory.java             | 31 +++++++++
 .../xwork2/ognl/StrutsProxyCacheFactory.java       | 44 ++++++++++++
 .../com/opensymphony/xwork2/util/ProxyUtil.java    | 80 +++++++++++++++++++---
 .../xwork2/util/StrutsProxyCacheFactoryBean.java   | 38 ++++++++++
 .../java/org/apache/struts2/StrutsConstants.java   | 14 ++++
 .../org/apache/struts2/default.properties          |  8 +++
 core/src/main/resources/struts-beans.xml           |  3 +
 8 files changed, 232 insertions(+), 33 deletions(-)

diff --git 
a/core/src/main/java/com/opensymphony/xwork2/config/impl/DefaultConfiguration.java
 
b/core/src/main/java/com/opensymphony/xwork2/config/impl/DefaultConfiguration.java
index d1560f53f..41a060c6c 100644
--- 
a/core/src/main/java/com/opensymphony/xwork2/config/impl/DefaultConfiguration.java
+++ 
b/core/src/main/java/com/opensymphony/xwork2/config/impl/DefaultConfiguration.java
@@ -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;
@@ -130,7 +133,7 @@ import java.util.TreeSet;
  * DefaultConfiguration
  *
  * @author Jason Carreira
- *         Created Feb 24, 2003 7:38:06 AM
+ * Created Feb 24, 2003 7:38:06 AM
  */
 public class DefaultConfiguration implements Configuration {
 
@@ -145,6 +148,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);
     }
@@ -224,7 +229,7 @@ public class DefaultConfiguration implements Configuration {
                         name, packageContext.getLocation());
             } else {
                 throw new ConfigurationException("The package name '" + name
-                        + "' at location "+packageContext.getLocation()
+                        + "' at location " + packageContext.getLocation()
                         + " is already been used by another package at 
location " + check.getLocation(),
                         packageContext);
             }
@@ -257,7 +262,6 @@ public class DefaultConfiguration implements Configuration {
      *
      * @param providers list of ContainerProvider
      * @return list of package providers
-     *
      * @throws ConfigurationException in case of any configuration errors
      */
     @Override
@@ -269,8 +273,7 @@ public class DefaultConfiguration implements Configuration {
         ContainerProperties props = new ContainerProperties();
         ContainerBuilder builder = new ContainerBuilder();
         Container bootstrap = createBootstrapContainer(providers);
-        for (final ContainerProvider containerProvider : providers)
-        {
+        for (final ContainerProvider containerProvider : providers) {
             bootstrap.inject(containerProvider);
             containerProvider.init(this);
             containerProvider.register(builder, props);
@@ -299,12 +302,11 @@ public class DefaultConfiguration implements 
Configuration {
             objectFactory = container.getInstance(ObjectFactory.class);
 
             // Process the configuration providers first
-            for (final ContainerProvider containerProvider : providers)
-            {
+            for (final ContainerProvider containerProvider : providers) {
                 if (containerProvider instanceof PackageProvider) {
                     container.inject(containerProvider);
-                    ((PackageProvider)containerProvider).loadPackages();
-                    packageProviders.add((PackageProvider)containerProvider);
+                    ((PackageProvider) containerProvider).loadPackages();
+                    packageProviders.add((PackageProvider) containerProvider);
                 }
             }
 
@@ -393,6 +395,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)
@@ -500,7 +504,7 @@ public class DefaultConfiguration implements Configuration {
             results.putAll(packageContext.getAllGlobalResults());
         }
 
-               results.putAll(baseConfig.getResults());
+        results.putAll(baseConfig.getResults());
 
         setDefaultResults(results, packageContext);
 
@@ -523,14 +527,14 @@ public class DefaultConfiguration implements 
Configuration {
         LOG.debug("Using pattern [{}] to match allowed methods when SMI is 
disabled!", methodRegex);
 
         return new ActionConfig.Builder(baseConfig)
-            .addParams(params)
-            .addResultConfigs(results)
-            .defaultClassName(packageContext.getDefaultClassRef())  // fill in 
default if non class has been provided
-            .interceptors(interceptors)
-            
.setStrictMethodInvocation(packageContext.isStrictMethodInvocation())
-            .setDefaultMethodRegex(methodRegex)
-            
.addExceptionMappings(packageContext.getAllExceptionMappingConfigs())
-            .build();
+                .addParams(params)
+                .addResultConfigs(results)
+                .defaultClassName(packageContext.getDefaultClassRef())  // 
fill in default if non class has been provided
+                .interceptors(interceptors)
+                
.setStrictMethodInvocation(packageContext.isStrictMethodInvocation())
+                .setDefaultMethodRegex(methodRegex)
+                
.addExceptionMappings(packageContext.getAllExceptionMappingConfigs())
+                .build();
     }
 
 
@@ -546,8 +550,7 @@ public class DefaultConfiguration implements Configuration {
                                         Map<String, String> namespaceConfigs,
                                         PatternMatcher<int[]> matcher,
                                         boolean appendNamedParameters,
-                                        boolean fallbackToEmptyNamespace)
-        {
+                                        boolean fallbackToEmptyNamespace) {
             this.namespaceActionConfigs = namespaceActionConfigs;
             this.namespaceConfigs = namespaceConfigs;
             this.fallbackToEmptyNamespace = fallbackToEmptyNamespace;
@@ -630,7 +633,7 @@ public class DefaultConfiguration implements Configuration {
          * @return a Map of namespace - > Map of ActionConfig objects, with 
the key being the action name
          */
         @Override
-        public Map<String, Map<String, ActionConfig>>  getActionConfigs() {
+        public Map<String, Map<String, ActionConfig>> getActionConfigs() {
             return namespaceActionConfigs;
         }
 
@@ -664,7 +667,7 @@ public class DefaultConfiguration implements Configuration {
 
         public void setConstants(ContainerBuilder builder) {
             for (Object keyobj : keySet()) {
-                String key = (String)keyobj;
+                String key = (String) keyobj;
                 builder.factory(String.class, key, new 
LocatableConstantFactory<>(getProperty(key), getPropertyLocation(key)));
             }
         }
diff --git 
a/core/src/main/java/com/opensymphony/xwork2/ognl/ProxyCacheFactory.java 
b/core/src/main/java/com/opensymphony/xwork2/ognl/ProxyCacheFactory.java
new file mode 100644
index 000000000..650e231f5
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/ognl/ProxyCacheFactory.java
@@ -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.8.0
+ */
+public interface ProxyCacheFactory<Key, Value> extends OgnlCacheFactory<Key, 
Value> {
+
+}
diff --git 
a/core/src/main/java/com/opensymphony/xwork2/ognl/StrutsProxyCacheFactory.java 
b/core/src/main/java/com/opensymphony/xwork2/ognl/StrutsProxyCacheFactory.java
new file mode 100644
index 000000000..ea2180f5f
--- /dev/null
+++ 
b/core/src/main/java/com/opensymphony/xwork2/ognl/StrutsProxyCacheFactory.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package 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.8.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));
+    }
+}
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java 
b/core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java
index 22c344446..a06d5941e 100644
--- a/core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java
+++ b/core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java
@@ -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,64 @@ 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
+    private static volatile OgnlCache<Class<?>, Boolean> isProxyCache;
+    private static volatile OgnlCache<Member, Boolean> isProxyMemberCache;
+
+    /**
+     * Sets the cache factory. Called by the container during initialization.
+     *
+     * @param factory the cache factory to use for creating proxy caches
+     * @since 6.8.0
+     */
+    public static void setProxyCacheFactory(ProxyCacheFactory<?, ?> factory) {
+        cacheFactory = factory;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static OgnlCache<Class<?>, Boolean> getIsProxyCache() {
+        if (isProxyCache == null) {
+            synchronized (ProxyUtil.class) {
+                if (isProxyCache == null) {
+                    isProxyCache = createCache();
+                }
+            }
+        }
+        return isProxyCache;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static OgnlCache<Member, Boolean> getIsProxyMemberCache() {
+        if (isProxyMemberCache == null) {
+            synchronized (ProxyUtil.class) {
+                if (isProxyMemberCache == null) {
+                    isProxyMemberCache = createCache();
+                }
+            }
+        }
+        return isProxyMemberCache;
+    }
+
+    @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 +127,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 = getIsProxyCache().get(clazz);
         if (flag != null) {
             return flag;
         }
 
         boolean isProxy = isSpringAopProxy(object) || isHibernateProxy(object);
 
-        isProxyCache.put(clazz, isProxy);
+        getIsProxyCache().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 +155,14 @@ public class ProxyUtil {
             return false;
         }
 
-        Boolean flag = isProxyMemberCache.get(member);
+        Boolean flag = getIsProxyMemberCache().get(member);
         if (flag != null) {
             return flag;
         }
 
         boolean isProxyMember = isSpringProxyMember(member) || 
isHibernateProxyMember(member);
 
-        isProxyMemberCache.put(member, isProxyMember);
+        getIsProxyMemberCache().put(member, isProxyMember);
         return isProxyMember;
     }
 
@@ -147,6 +198,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 +222,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 +233,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 +255,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 +276,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 +285,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 +300,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) {
diff --git 
a/core/src/main/java/com/opensymphony/xwork2/util/StrutsProxyCacheFactoryBean.java
 
b/core/src/main/java/com/opensymphony/xwork2/util/StrutsProxyCacheFactoryBean.java
new file mode 100644
index 000000000..679ae2516
--- /dev/null
+++ 
b/core/src/main/java/com/opensymphony/xwork2/util/StrutsProxyCacheFactoryBean.java
@@ -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.8.0
+ */
+public class StrutsProxyCacheFactoryBean {
+
+    @Inject
+    public StrutsProxyCacheFactoryBean(ProxyCacheFactory<?, ?> 
proxyCacheFactory) {
+        ProxyUtil.setProxyCacheFactory(proxyCacheFactory);
+    }
+}
diff --git a/core/src/main/java/org/apache/struts2/StrutsConstants.java 
b/core/src/main/java/org/apache/struts2/StrutsConstants.java
index 44cb01468..f2db5eccd 100644
--- a/core/src/main/java/org/apache/struts2/StrutsConstants.java
+++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java
@@ -516,4 +516,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";
 }
diff --git a/core/src/main/resources/org/apache/struts2/default.properties 
b/core/src/main/resources/org/apache/struts2/default.properties
index 7dd782ccb..407539eee 100644
--- a/core/src/main/resources/org/apache/struts2/default.properties
+++ b/core/src/main/resources/org/apache/struts2/default.properties
@@ -247,6 +247,14 @@ 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 'basic' (no Caffeine 
dependency required).
+### Use 'wtlfu' for better eviction policy if Caffeine is available.
+struts.proxy.cacheType=basic
+
+### 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
diff --git a/core/src/main/resources/struts-beans.xml 
b/core/src/main/resources/struts-beans.xml
index 6aad85ca6..132510a09 100644
--- a/core/src/main/resources/struts-beans.xml
+++ b/core/src/main/resources/struts-beans.xml
@@ -252,6 +252,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"/>

Reply via email to