Skip to content

Commit 18d6e77

Browse files
lukaszlenartclaude
andauthored
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>
1 parent 2bd1b60 commit 18d6e77

9 files changed

Lines changed: 265 additions & 11 deletions

File tree

core/src/main/java/com/opensymphony/xwork2/config/impl/DefaultConfiguration.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@
8282
import com.opensymphony.xwork2.ognl.DefaultOgnlBeanInfoCacheFactory;
8383
import com.opensymphony.xwork2.ognl.DefaultOgnlExpressionCacheFactory;
8484
import com.opensymphony.xwork2.ognl.ExpressionCacheFactory;
85+
import com.opensymphony.xwork2.ognl.ProxyCacheFactory;
86+
import com.opensymphony.xwork2.ognl.StrutsProxyCacheFactory;
8587
import com.opensymphony.xwork2.ognl.OgnlCacheFactory;
8688
import com.opensymphony.xwork2.ognl.OgnlReflectionProvider;
8789
import com.opensymphony.xwork2.ognl.OgnlUtil;
@@ -93,6 +95,7 @@
9395
import com.opensymphony.xwork2.util.OgnlTextParser;
9496
import com.opensymphony.xwork2.util.PatternMatcher;
9597
import com.opensymphony.xwork2.util.StrutsLocalizedTextProvider;
98+
import com.opensymphony.xwork2.util.StrutsProxyCacheFactoryBean;
9699
import com.opensymphony.xwork2.util.TextParser;
97100
import com.opensymphony.xwork2.util.ValueStack;
98101
import com.opensymphony.xwork2.util.ValueStackFactory;
@@ -143,6 +146,8 @@ public class DefaultConfiguration implements Configuration {
143146
constants.put(StrutsConstants.STRUTS_OGNL_EXPRESSION_CACHE_MAXSIZE, 10000);
144147
constants.put(StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_TYPE, OgnlCacheFactory.CacheType.BASIC);
145148
constants.put(StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_MAXSIZE, 10000);
149+
constants.put(StrutsConstants.STRUTS_PROXY_CACHE_TYPE, OgnlCacheFactory.CacheType.BASIC);
150+
constants.put(StrutsConstants.STRUTS_PROXY_CACHE_MAXSIZE, 10000);
146151
constants.put(StrutsConstants.STRUTS_ENABLE_DYNAMIC_METHOD_INVOCATION, Boolean.FALSE);
147152
BOOTSTRAP_CONSTANTS = Collections.unmodifiableMap(constants);
148153
}
@@ -394,6 +399,8 @@ public static ContainerBuilder bootstrapFactories(ContainerBuilder builder) {
394399

395400
.factory(ExpressionCacheFactory.class, DefaultOgnlExpressionCacheFactory.class, Scope.SINGLETON)
396401
.factory(BeanInfoCacheFactory.class, DefaultOgnlBeanInfoCacheFactory.class, Scope.SINGLETON)
402+
.factory(ProxyCacheFactory.class, StrutsProxyCacheFactory.class, Scope.SINGLETON)
403+
.factory(StrutsProxyCacheFactoryBean.class, Scope.SINGLETON)
397404
.factory(OgnlUtil.class, Scope.SINGLETON)
398405
.factory(SecurityMemberAccess.class, Scope.PROTOTYPE)
399406
.factory(OgnlGuard.class, StrutsOgnlGuard.class, Scope.SINGLETON)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package com.opensymphony.xwork2.ognl;
20+
21+
/**
22+
* A factory interface for ProxyUtil cache to be used with Struts DI mechanism.
23+
* This allows the proxy detection cache type to be configurable via Struts constants.
24+
*
25+
* @param <Key> The type for the cache key entries
26+
* @param <Value> The type for the cache value entries
27+
* @since 6.9.0
28+
*/
29+
public interface ProxyCacheFactory<Key, Value> extends OgnlCacheFactory<Key, Value> {
30+
31+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package com.opensymphony.xwork2.ognl;
20+
21+
import com.opensymphony.xwork2.inject.Inject;
22+
import org.apache.commons.lang3.EnumUtils;
23+
import org.apache.struts2.StrutsConstants;
24+
25+
/**
26+
* Struts Proxy Cache factory implementation for ProxyUtil caches.
27+
* <p>
28+
* This factory is used to create caches for proxy detection in ProxyUtil.
29+
* The cache type and size can be configured via Struts constants.
30+
*
31+
* @param <Key> The type for the cache key entries
32+
* @param <Value> The type for the cache value entries
33+
* @since 6.9.0
34+
*/
35+
public class StrutsProxyCacheFactory<Key, Value> extends DefaultOgnlCacheFactory<Key, Value>
36+
implements ProxyCacheFactory<Key, Value> {
37+
38+
@Inject
39+
public StrutsProxyCacheFactory(
40+
@Inject(value = StrutsConstants.STRUTS_PROXY_CACHE_MAXSIZE) String cacheMaxSize,
41+
@Inject(value = StrutsConstants.STRUTS_PROXY_CACHE_TYPE) String defaultCacheType) {
42+
super(Integer.parseInt(cacheMaxSize), EnumUtils.getEnumIgnoreCase(CacheType.class, defaultCacheType));
43+
}
44+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package com.opensymphony.xwork2.util;
20+
21+
import java.util.function.Supplier;
22+
23+
/**
24+
* A thread-safe lazy reference that computes its value on first access using
25+
* double-checked locking. The cached value can be invalidated via {@link #reset()},
26+
* causing the next {@link #get()} call to recompute the value.
27+
*
28+
* @param <T> the type of the lazily computed value
29+
* @since 6.9.0
30+
*/
31+
public class LazyRef<T> implements Supplier<T> {
32+
33+
private final Supplier<T> factory;
34+
private volatile T value;
35+
36+
/**
37+
* Creates a new LazyRef with the given factory supplier.
38+
*
39+
* @param factory the supplier used to compute the value; must not be null
40+
*/
41+
public LazyRef(Supplier<T> factory) {
42+
this.factory = factory;
43+
}
44+
45+
/**
46+
* Returns the cached value, computing it on first access or after a {@link #reset()}.
47+
*
48+
* @return the computed value
49+
*/
50+
@Override
51+
public T get() {
52+
T result = value;
53+
if (result == null) {
54+
synchronized (this) {
55+
result = value;
56+
if (result == null) {
57+
result = factory.get();
58+
value = result;
59+
}
60+
}
61+
}
62+
return result;
63+
}
64+
65+
/**
66+
* Invalidates the cached value so the next {@link #get()} call recomputes it.
67+
*/
68+
public void reset() {
69+
value = null;
70+
}
71+
}

core/src/main/java/com/opensymphony/xwork2/util/ProxyUtil.java

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.opensymphony.xwork2.ognl.DefaultOgnlCacheFactory;
2222
import com.opensymphony.xwork2.ognl.OgnlCache;
2323
import com.opensymphony.xwork2.ognl.OgnlCacheFactory;
24+
import com.opensymphony.xwork2.ognl.ProxyCacheFactory;
2425
import org.apache.commons.lang3.reflect.ConstructorUtils;
2526
import org.apache.commons.lang3.reflect.FieldUtils;
2627
import org.apache.commons.lang3.reflect.MethodUtils;
@@ -41,7 +42,6 @@
4142
* <p>
4243
* Various utility methods dealing with proxies
4344
* </p>
44-
*
4545
*/
4646
public class ProxyUtil {
4747
private static final String SPRING_ADVISED_CLASS_NAME = "org.springframework.aop.framework.Advised";
@@ -51,15 +51,45 @@ public class ProxyUtil {
5151
private static final String HIBERNATE_HIBERNATEPROXY_CLASS_NAME = "org.hibernate.proxy.HibernateProxy";
5252
private static final int CACHE_MAX_SIZE = 10000;
5353
private static final int CACHE_INITIAL_CAPACITY = 256;
54-
private static final OgnlCache<Class<?>, Boolean> isProxyCache = new DefaultOgnlCacheFactory<Class<?>, Boolean>(
55-
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache();
56-
private static final OgnlCache<Member, Boolean> isProxyMemberCache = new DefaultOgnlCacheFactory<Member, Boolean>(
57-
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache();
54+
55+
// Holder for the cache factory (set by container)
56+
private static volatile ProxyCacheFactory<?, ?> cacheFactory;
57+
58+
// Lazy-initialized caches with reset support
59+
private static final LazyRef<OgnlCache<Class<?>, Boolean>> isProxyCache =
60+
new LazyRef<>(ProxyUtil::createCache);
61+
private static final LazyRef<OgnlCache<Member, Boolean>> isProxyMemberCache =
62+
new LazyRef<>(ProxyUtil::createCache);
63+
64+
/**
65+
* Sets the cache factory. Called by the container during initialization.
66+
* Resets existing caches so they are recreated with the new factory.
67+
*
68+
* @param factory the cache factory to use for creating proxy caches
69+
* @since 6.9.0
70+
*/
71+
public static void setProxyCacheFactory(ProxyCacheFactory<?, ?> factory) {
72+
cacheFactory = factory;
73+
isProxyCache.reset();
74+
isProxyMemberCache.reset();
75+
}
76+
77+
@SuppressWarnings("unchecked")
78+
private static <K, V> OgnlCache<K, V> createCache() {
79+
if (cacheFactory != null) {
80+
return ((ProxyCacheFactory<K, V>) cacheFactory).buildOgnlCache(
81+
CACHE_MAX_SIZE, CACHE_INITIAL_CAPACITY, 0.75f, cacheFactory.getDefaultCacheType());
82+
}
83+
// Fallback to BASIC if container hasn't initialized yet
84+
return new DefaultOgnlCacheFactory<K, V>(
85+
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.BASIC, CACHE_INITIAL_CAPACITY).buildOgnlCache();
86+
}
5887

5988
/**
6089
* Determine the ultimate target class of the given instance, traversing
6190
* not only a top-level proxy but any number of nested proxies as well &mdash;
6291
* as long as possible without side effects.
92+
*
6393
* @param candidate the instance to check (might be a proxy)
6494
* @return the ultimate target class (or the plain class of the given
6595
* object as fallback; never {@code null})
@@ -78,24 +108,26 @@ public static Class<?> ultimateTargetClass(Object candidate) {
78108

79109
/**
80110
* Check whether the given object is a proxy.
111+
*
81112
* @param object the object to check
82113
*/
83114
public static boolean isProxy(Object object) {
84115
if (object == null) return false;
85116
Class<?> clazz = object.getClass();
86-
Boolean flag = isProxyCache.get(clazz);
117+
Boolean flag = isProxyCache.get().get(clazz);
87118
if (flag != null) {
88119
return flag;
89120
}
90121

91122
boolean isProxy = isSpringAopProxy(object) || isHibernateProxy(object);
92123

93-
isProxyCache.put(clazz, isProxy);
124+
isProxyCache.get().put(clazz, isProxy);
94125
return isProxy;
95126
}
96127

97128
/**
98129
* Check whether the given member is a proxy member of a proxy object or is a static proxy member.
130+
*
99131
* @param member the member to check
100132
* @param object the object to check
101133
*/
@@ -104,14 +136,14 @@ public static boolean isProxyMember(Member member, Object object) {
104136
return false;
105137
}
106138

107-
Boolean flag = isProxyMemberCache.get(member);
139+
Boolean flag = isProxyMemberCache.get().get(member);
108140
if (flag != null) {
109141
return flag;
110142
}
111143

112144
boolean isProxyMember = isSpringProxyMember(member) || isHibernateProxyMember(member);
113145

114-
isProxyMemberCache.put(member, isProxyMember);
146+
isProxyMemberCache.get().put(member, isProxyMember);
115147
return isProxyMember;
116148
}
117149

@@ -147,6 +179,7 @@ public static boolean isHibernateProxyMember(Member member) {
147179
* Determine the ultimate target class of the given spring bean instance, traversing
148180
* not only a top-level spring proxy but any number of nested spring proxies as well &mdash;
149181
* as long as possible without side effects, that is, just for singleton targets.
182+
*
150183
* @param candidate the instance to check (might be a spring AOP proxy)
151184
* @return the ultimate target class (or the plain class of the given
152185
* object as fallback; never {@code null})
@@ -170,6 +203,7 @@ private static Class<?> springUltimateTargetClass(Object candidate) {
170203

171204
/**
172205
* Check whether the given object is a Spring proxy.
206+
*
173207
* @param object the object to check
174208
*/
175209
private static boolean isSpringAopProxy(Object object) {
@@ -180,6 +214,7 @@ private static boolean isSpringAopProxy(Object object) {
180214

181215
/**
182216
* Check whether the given member is a member of a spring proxy.
217+
*
183218
* @param member the member to check
184219
*/
185220
private static boolean isSpringProxyMember(Member member) {
@@ -201,6 +236,7 @@ private static boolean isSpringProxyMember(Member member) {
201236

202237
/**
203238
* Obtain the singleton target object behind the given spring proxy, if any.
239+
*
204240
* @param candidate the (potential) spring proxy to check
205241
* @return the singleton target object, or {@code null} in any other case
206242
* (not a spring proxy, not an existing singleton target)
@@ -221,6 +257,7 @@ private static Object getSingletonTarget(Object candidate) {
221257

222258
/**
223259
* Check whether the specified class is a CGLIB-generated class.
260+
*
224261
* @param clazz the class to check
225262
*/
226263
private static boolean isCglibProxyClass(Class<?> clazz) {
@@ -229,7 +266,8 @@ private static boolean isCglibProxyClass(Class<?> clazz) {
229266

230267
/**
231268
* Check whether the given class implements an interface with a given class name.
232-
* @param clazz the class to check
269+
*
270+
* @param clazz the class to check
233271
* @param ifaceClassName the interface class name to check
234272
*/
235273
private static boolean implementsInterface(Class<?> clazz, String ifaceClassName) {
@@ -243,7 +281,8 @@ private static boolean implementsInterface(Class<?> clazz, String ifaceClassName
243281

244282
/**
245283
* Check whether the given class has a given member.
246-
* @param clazz the class to check
284+
*
285+
* @param clazz the class to check
247286
* @param member the member to check
248287
*/
249288
private static boolean hasMember(Class<?> clazz, Member member) {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package com.opensymphony.xwork2.util;
20+
21+
import com.opensymphony.xwork2.inject.Inject;
22+
import com.opensymphony.xwork2.ognl.ProxyCacheFactory;
23+
24+
/**
25+
* Bean that wires the ProxyCacheFactory to ProxyUtil during container initialization.
26+
* <p>
27+
* This bean is created by the container and receives the configured ProxyCacheFactory
28+
* via dependency injection, then passes it to the static ProxyUtil class.
29+
*
30+
* @since 6.9.0
31+
*/
32+
public class StrutsProxyCacheFactoryBean {
33+
34+
@Inject
35+
public StrutsProxyCacheFactoryBean(ProxyCacheFactory<?, ?> proxyCacheFactory) {
36+
ProxyUtil.setProxyCacheFactory(proxyCacheFactory);
37+
}
38+
}

0 commit comments

Comments
 (0)