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 — * 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 — * 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"/>
