From bbc2afc5d96837e299102fd51414f33b4e6521be Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Mon, 9 Feb 2026 19:02:29 +0200 Subject: [PATCH 1/2] fix(ognl): make ProxyUtil cache configurable via struts constants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../config/impl/DefaultConfiguration.java | 7 ++ .../xwork2/ognl/ProxyCacheFactory.java | 31 +++++++ .../xwork2/ognl/StrutsProxyCacheFactory.java | 44 ++++++++++ .../opensymphony/xwork2/util/ProxyUtil.java | 80 ++++++++++++++++--- .../util/StrutsProxyCacheFactoryBean.java | 38 +++++++++ .../org/apache/struts2/StrutsConstants.java | 14 ++++ .../org/apache/struts2/default.properties | 8 ++ core/src/main/resources/struts-beans.xml | 3 + 8 files changed, 214 insertions(+), 11 deletions(-) create mode 100644 core/src/main/java/com/opensymphony/xwork2/ognl/ProxyCacheFactory.java create mode 100644 core/src/main/java/com/opensymphony/xwork2/ognl/StrutsProxyCacheFactory.java create mode 100644 core/src/main/java/com/opensymphony/xwork2/util/StrutsProxyCacheFactoryBean.java 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 8990ba129e..a2f73879df 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.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.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 static ContainerBuilder bootstrapFactories(ContainerBuilder builder) { .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) 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 0000000000..650e231f54 --- /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 The type for the cache key entries + * @param The type for the cache value entries + * @since 6.8.0 + */ +public interface ProxyCacheFactory extends OgnlCacheFactory { + +} 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 0000000000..ea2180f5fe --- /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. + *

+ * This factory is used to create caches for proxy detection in ProxyUtil. + * The cache type and size can be configured via Struts constants. + * + * @param The type for the cache key entries + * @param The type for the cache value entries + * @since 6.8.0 + */ +public class StrutsProxyCacheFactory extends DefaultOgnlCacheFactory + implements ProxyCacheFactory { + + @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 22c3444466..a06d5941ea 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 @@ 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 @@ *

* Various utility methods dealing with proxies *

- * */ 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, Boolean> isProxyCache = new DefaultOgnlCacheFactory, Boolean>( - CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache(); - private static final OgnlCache isProxyMemberCache = new DefaultOgnlCacheFactory( - 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, Boolean> isProxyCache; + private static volatile OgnlCache 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, Boolean> getIsProxyCache() { + if (isProxyCache == null) { + synchronized (ProxyUtil.class) { + if (isProxyCache == null) { + isProxyCache = createCache(); + } + } + } + return isProxyCache; + } + + @SuppressWarnings("unchecked") + private static OgnlCache getIsProxyMemberCache() { + if (isProxyMemberCache == null) { + synchronized (ProxyUtil.class) { + if (isProxyMemberCache == null) { + isProxyMemberCache = createCache(); + } + } + } + return isProxyMemberCache; + } + + @SuppressWarnings("unchecked") + private static OgnlCache createCache() { + if (cacheFactory != null) { + return ((ProxyCacheFactory) cacheFactory).buildOgnlCache( + CACHE_MAX_SIZE, CACHE_INITIAL_CAPACITY, 0.75f, cacheFactory.getDefaultCacheType()); + } + // Fallback to BASIC if container hasn't initialized yet + return new DefaultOgnlCacheFactory( + 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 — * 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 static Class ultimateTargetClass(Object candidate) { /** * 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 static boolean isProxyMember(Member member, Object object) { 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 static boolean isHibernateProxyMember(Member member) { * 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 — * 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 @@ private static Class springUltimateTargetClass(Object candidate) { /** * 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 @@ private static boolean isSpringAopProxy(Object object) { /** * 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 @@ private static boolean isSpringProxyMember(Member member) { /** * 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 @@ private static Object getSingletonTarget(Object candidate) { /** * 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 @@ private static boolean isCglibProxyClass(Class clazz) { /** * 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 @@ private static boolean implementsInterface(Class clazz, String ifaceClassName /** * 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 0000000000..679ae25168 --- /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. + *

+ * 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 e666df61aa..0ac751640e 100644 --- a/core/src/main/java/org/apache/struts2/StrutsConstants.java +++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java @@ -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"; } diff --git a/core/src/main/resources/org/apache/struts2/default.properties b/core/src/main/resources/org/apache/struts2/default.properties index 7dd782ccb1..407539eee6 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 be1838664b..bb0ede4809 100644 --- a/core/src/main/resources/struts-beans.xml +++ b/core/src/main/resources/struts-beans.xml @@ -255,6 +255,9 @@ class="com.opensymphony.xwork2.ognl.DefaultOgnlExpressionCacheFactory" scope="singleton"/> + + From 4cb5f359cf87c77cc7108597eedf6e585585c0c3 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Sat, 21 Feb 2026 06:32:32 +0100 Subject: [PATCH 2/2] refactor(ognl): use LazyRef for proxy caches and reset on factory change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract lazy initialization into reusable LazyRef 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 --- .../xwork2/ognl/ProxyCacheFactory.java | 2 +- .../xwork2/ognl/StrutsProxyCacheFactory.java | 2 +- .../com/opensymphony/xwork2/util/LazyRef.java | 71 +++++++++++++++++++ .../opensymphony/xwork2/util/ProxyUtil.java | 45 ++++-------- .../util/StrutsProxyCacheFactoryBean.java | 2 +- .../org/apache/struts2/default.properties | 5 +- 6 files changed, 89 insertions(+), 38 deletions(-) create mode 100644 core/src/main/java/com/opensymphony/xwork2/util/LazyRef.java diff --git a/core/src/main/java/com/opensymphony/xwork2/ognl/ProxyCacheFactory.java b/core/src/main/java/com/opensymphony/xwork2/ognl/ProxyCacheFactory.java index 650e231f54..4bb63453a2 100644 --- a/core/src/main/java/com/opensymphony/xwork2/ognl/ProxyCacheFactory.java +++ b/core/src/main/java/com/opensymphony/xwork2/ognl/ProxyCacheFactory.java @@ -24,7 +24,7 @@ * * @param The type for the cache key entries * @param The type for the cache value entries - * @since 6.8.0 + * @since 6.9.0 */ public interface ProxyCacheFactory extends OgnlCacheFactory { diff --git a/core/src/main/java/com/opensymphony/xwork2/ognl/StrutsProxyCacheFactory.java b/core/src/main/java/com/opensymphony/xwork2/ognl/StrutsProxyCacheFactory.java index ea2180f5fe..9f45719966 100644 --- a/core/src/main/java/com/opensymphony/xwork2/ognl/StrutsProxyCacheFactory.java +++ b/core/src/main/java/com/opensymphony/xwork2/ognl/StrutsProxyCacheFactory.java @@ -30,7 +30,7 @@ * * @param The type for the cache key entries * @param The type for the cache value entries - * @since 6.8.0 + * @since 6.9.0 */ public class StrutsProxyCacheFactory extends DefaultOgnlCacheFactory implements ProxyCacheFactory { diff --git a/core/src/main/java/com/opensymphony/xwork2/util/LazyRef.java b/core/src/main/java/com/opensymphony/xwork2/util/LazyRef.java new file mode 100644 index 0000000000..113ca6bc9e --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/util/LazyRef.java @@ -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 the type of the lazily computed value + * @since 6.9.0 + */ +public class LazyRef implements Supplier { + + private final Supplier 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 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; + } +} 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 a06d5941ea..c3c4e11332 100644 --- a/core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java +++ b/core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java @@ -55,42 +55,23 @@ public class ProxyUtil { // Holder for the cache factory (set by container) private static volatile ProxyCacheFactory cacheFactory; - // Lazy-initialized caches - private static volatile OgnlCache, Boolean> isProxyCache; - private static volatile OgnlCache isProxyMemberCache; + // Lazy-initialized caches with reset support + private static final LazyRef, Boolean>> isProxyCache = + new LazyRef<>(ProxyUtil::createCache); + private static final LazyRef> 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.8.0 + * @since 6.9.0 */ public static void setProxyCacheFactory(ProxyCacheFactory factory) { cacheFactory = factory; - } - - @SuppressWarnings("unchecked") - private static OgnlCache, Boolean> getIsProxyCache() { - if (isProxyCache == null) { - synchronized (ProxyUtil.class) { - if (isProxyCache == null) { - isProxyCache = createCache(); - } - } - } - return isProxyCache; - } - - @SuppressWarnings("unchecked") - private static OgnlCache getIsProxyMemberCache() { - if (isProxyMemberCache == null) { - synchronized (ProxyUtil.class) { - if (isProxyMemberCache == null) { - isProxyMemberCache = createCache(); - } - } - } - return isProxyMemberCache; + isProxyCache.reset(); + isProxyMemberCache.reset(); } @SuppressWarnings("unchecked") @@ -133,14 +114,14 @@ public static Class ultimateTargetClass(Object candidate) { public static boolean isProxy(Object object) { if (object == null) return false; Class clazz = object.getClass(); - Boolean flag = getIsProxyCache().get(clazz); + Boolean flag = isProxyCache.get().get(clazz); if (flag != null) { return flag; } boolean isProxy = isSpringAopProxy(object) || isHibernateProxy(object); - getIsProxyCache().put(clazz, isProxy); + isProxyCache.get().put(clazz, isProxy); return isProxy; } @@ -155,14 +136,14 @@ public static boolean isProxyMember(Member member, Object object) { return false; } - Boolean flag = getIsProxyMemberCache().get(member); + Boolean flag = isProxyMemberCache.get().get(member); if (flag != null) { return flag; } boolean isProxyMember = isSpringProxyMember(member) || isHibernateProxyMember(member); - getIsProxyMemberCache().put(member, isProxyMember); + isProxyMemberCache.get().put(member, isProxyMember); return isProxyMember; } diff --git a/core/src/main/java/com/opensymphony/xwork2/util/StrutsProxyCacheFactoryBean.java b/core/src/main/java/com/opensymphony/xwork2/util/StrutsProxyCacheFactoryBean.java index 679ae25168..1cd1d2d406 100644 --- a/core/src/main/java/com/opensymphony/xwork2/util/StrutsProxyCacheFactoryBean.java +++ b/core/src/main/java/com/opensymphony/xwork2/util/StrutsProxyCacheFactoryBean.java @@ -27,7 +27,7 @@ * 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 + * @since 6.9.0 */ public class StrutsProxyCacheFactoryBean { diff --git a/core/src/main/resources/org/apache/struts2/default.properties b/core/src/main/resources/org/apache/struts2/default.properties index 407539eee6..3eedc24372 100644 --- a/core/src/main/resources/org/apache/struts2/default.properties +++ b/core/src/main/resources/org/apache/struts2/default.properties @@ -248,9 +248,8 @@ struts.ognl.beanInfoCacheType=wtlfu 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 +### 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