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"/> + +