This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git
The following commit(s) were added to refs/heads/main by this push:
new fd8742586 fix(spring): WW-3647 change autowire alwaysRespect default
to true (#1571)
fd8742586 is described below
commit fd874258631e999f5bd5ffc3fea8c0e416c61962
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon Feb 9 10:08:33 2026 +0200
fix(spring): WW-3647 change autowire alwaysRespect default to true (#1571)
Change the default value of
struts.objectFactory.spring.autoWire.alwaysRespect
from false to true to fix the Spring constructor autowiring issue.
When a Spring String bean exists (e.g., JNDI lookup with default-value),
Spring's AUTOWIRE_CONSTRUCTOR strategy incorrectly injects that value into
ALL String parameters of ServletActionRedirectResult constructors, causing
malformed redirect URLs.
Setting alwaysRespect to true by default ensures the configured autowire
strategy (AUTOWIRE_BY_NAME) is consistently used, preventing unintended
bean injection.
Users who rely on the legacy constructor autowiring behavior can restore
it by setting:
<constant name="struts.objectFactory.spring.autoWire.alwaysRespect"
value="false" />
Fixes https://issues.apache.org/jira/browse/WW-3647
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <[email protected]>
---
.../java/org/apache/struts2/StrutsConstants.java | 10 +-
.../org/apache/struts2/default.properties | 4 +-
.../apache/struts2/spring/SpringObjectFactory.java | 24 ++-
...-02-07-WW-3647-spring-constructor-autowiring.md | 200 +++++++++++++++++++++
4 files changed, 221 insertions(+), 17 deletions(-)
diff --git a/core/src/main/java/org/apache/struts2/StrutsConstants.java
b/core/src/main/java/org/apache/struts2/StrutsConstants.java
index a66478a97..c7e1dd916 100644
--- a/core/src/main/java/org/apache/struts2/StrutsConstants.java
+++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java
@@ -256,9 +256,15 @@ public final class StrutsConstants {
public static final String STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE =
"struts.objectFactory.spring.autoWire";
/**
- * Whether the autowire strategy chosen by
STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE is always respected. Defaults
- * to false, which is the legacy behavior that tries to determine the best
strategy for the situation.
+ * Whether the autowire strategy chosen by
STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE is always respected.
+ * Defaults to true, which ensures the configured autowire strategy
(AUTOWIRE_BY_NAME by default) is
+ * consistently used. This prevents issues where Spring's
AUTOWIRE_CONSTRUCTOR strategy could inject
+ * unintended beans (e.g., String beans) into constructors.
+ * <p>
+ * Set to false to restore legacy behavior that mixes injection
strategies, but be aware this can
+ * cause issues like WW-3647 where String beans are incorrectly injected
into result class constructors.
*
+ * @see <a href="https://issues.apache.org/jira/browse/WW-3647">WW-3647</a>
* @since 2.1.3
*/
public static final String
STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE_ALWAYS_RESPECT =
"struts.objectFactory.spring.autoWire.alwaysRespect";
diff --git a/core/src/main/resources/org/apache/struts2/default.properties
b/core/src/main/resources/org/apache/struts2/default.properties
index a980196a6..59165eca3 100644
--- a/core/src/main/resources/org/apache/struts2/default.properties
+++ b/core/src/main/resources/org/apache/struts2/default.properties
@@ -42,8 +42,8 @@ struts.objectFactory.spring.autoWire = name
struts.objectFactory.spring.useClassCache = true
### ensures the autowire strategy is always respected.
-### valid values are: true, false (false is the default)
-struts.objectFactory.spring.autoWire.alwaysRespect = false
+### valid values are: true, false (true is the default)
+struts.objectFactory.spring.autoWire.alwaysRespect=true
### By default SpringObjectFactory doesn't support AOP
### This flag was added just temporally to check if nothing is broken
diff --git
a/plugins/spring/src/main/java/org/apache/struts2/spring/SpringObjectFactory.java
b/plugins/spring/src/main/java/org/apache/struts2/spring/SpringObjectFactory.java
index 2cfd97b66..e6cd2195b 100644
---
a/plugins/spring/src/main/java/org/apache/struts2/spring/SpringObjectFactory.java
+++
b/plugins/spring/src/main/java/org/apache/struts2/spring/SpringObjectFactory.java
@@ -18,11 +18,11 @@
*/
package org.apache.struts2.spring;
-import org.apache.struts2.ObjectFactory;
-import org.apache.struts2.inject.Inject;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.apache.struts2.ObjectFactory;
+import org.apache.struts2.inject.Inject;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
@@ -55,9 +55,10 @@ public class SpringObjectFactory extends ObjectFactory
implements ApplicationCon
protected int autowireStrategy =
AutowireCapableBeanFactory.AUTOWIRE_BY_NAME;
private final Map<String, Object> classes = new HashMap<>();
private boolean useClassCache = true;
- private boolean alwaysRespectAutowireStrategy = false;
+ private boolean alwaysRespectAutowireStrategy = true;
/**
* This is temporary solution, after validating can be removed
+ *
* @since 2.3.18
*/
@Deprecated
@@ -66,7 +67,7 @@ public class SpringObjectFactory extends ObjectFactory
implements ApplicationCon
public SpringObjectFactory() {
}
- @Inject(value="applicationContextPath",required=false)
+ @Inject(value = "applicationContextPath", required = false)
public void setApplicationContextPath(String ctx) {
if (ctx != null) {
setApplicationContext(new ClassPathXmlApplicationContext(ctx));
@@ -130,7 +131,6 @@ public class SpringObjectFactory extends ObjectFactory
implements ApplicationCon
* set the autoWiringFactory appropriately.
*
* @param context the application context
- *
* @return the bean factory
*/
protected AutowireCapableBeanFactory
findAutoWiringBeanFactory(ApplicationContext context) {
@@ -153,8 +153,7 @@ public class SpringObjectFactory extends ObjectFactory
implements ApplicationCon
*
* @param beanName The name of the bean to look up in the application
context
* @param extraContext additional context parameters
- * @return A bean from Spring or the result of calling the overridden
- * method.
+ * @return A bean from Spring or the result of calling the overridden
method.
* @throws Exception in case of any errors
*/
@Override
@@ -174,7 +173,7 @@ public class SpringObjectFactory extends ObjectFactory
implements ApplicationCon
}
/**
- * @param clazz class of bean
+ * @param clazz class of bean
* @param extraContext additional context parameters
* @return bean
* @throws Exception in case of any errors
@@ -212,9 +211,8 @@ public class SpringObjectFactory extends ObjectFactory
implements ApplicationCon
}
/**
- * @param bean the bean to be autowired
+ * @param bean the bean to be autowired
* @param autoWiringFactory the autowiring factory
- *
* @return bean
*/
public Object autoWireBean(Object bean, AutowireCapableBeanFactory
autoWiringFactory) {
@@ -235,7 +233,7 @@ public class SpringObjectFactory extends ObjectFactory
implements ApplicationCon
public Class getClassInstance(String className) throws
ClassNotFoundException {
Class clazz = null;
if (useClassCache) {
- synchronized(classes) {
+ synchronized (classes) {
// this cache of classes is needed because Spring sucks at
dealing with situations where the
// class instance changes
clazz = (Class) classes.get(className);
@@ -250,7 +248,7 @@ public class SpringObjectFactory extends ObjectFactory
implements ApplicationCon
}
if (useClassCache) {
- synchronized(classes) {
+ synchronized (classes) {
classes.put(className, clazz);
}
}
@@ -271,7 +269,7 @@ public class SpringObjectFactory extends ObjectFactory
implements ApplicationCon
}
/**
- * Enable / disable caching of classes loaded by Spring.
+ * Enable / disable caching of classes loaded by Spring.
*
* @param useClassCache enable / disable class cache
*/
diff --git
a/thoughts/shared/research/2026-02-07-WW-3647-spring-constructor-autowiring.md
b/thoughts/shared/research/2026-02-07-WW-3647-spring-constructor-autowiring.md
new file mode 100644
index 000000000..75c884731
--- /dev/null
+++
b/thoughts/shared/research/2026-02-07-WW-3647-spring-constructor-autowiring.md
@@ -0,0 +1,200 @@
+---
+date: 2026-02-07T12:00:00+01:00
+topic: "WW-3647: ServletActionRedirectResult Spring Constructor Autowiring
Issue"
+tags: [ research, codebase, spring-plugin, autowiring, constructor-injection ]
+status: complete
+---
+
+# Research: WW-3647 ServletActionRedirectResult Spring Constructor Autowiring
Issue
+
+**Date**: 2026-02-07
+
+## Research Question
+
+Is WW-3647 (ServletActionRedirectResult injection problem with Spring JNDI
lookup beans) still an issue, and what is the
+root cause?
+
+## Summary
+
+**YES, this issue is still present in the current codebase.** The root cause
is the default "mixed injection strategy"
+in `SpringObjectFactory` that uses `AUTOWIRE_CONSTRUCTOR`, which causes Spring
to inject any available String bean into
+all String parameters of result class constructors.
+
+## Detailed Findings
+
+### The Problem
+
+When a Spring JNDI lookup bean with a String default value exists:
+
+```xml
+
+<jee:jndi-lookup jndi-name="someName" id="currentEnvironment"
default-value="XXXX"/>
+```
+
+`ServletActionRedirectResult` redirect URLs become malformed:
+
+```
+http://localhost:8080/XXXX/index!XXXX.action#XXXX
+```
+
+### Root Cause
+
+The issue is in `SpringObjectFactory.buildBean(Class, Map)` at line 199:
+
+```java
+// When alwaysRespectAutowireStrategy is false (DEFAULT)
+bean =autoWiringFactory.
+
+autowire(clazz, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false);
+```
+
+Spring's `AUTOWIRE_CONSTRUCTOR` strategy:
+
+1. Examines all constructors of `ServletActionRedirectResult`
+2. Finds constructors with String parameters
+3. Looks for Spring beans of type `String` to inject
+4. Injects the JNDI bean's String value into **ALL** String parameters
+
+### ServletActionRedirectResult Constructors
+
+The class has 5 constructors, 4 of which take String parameters (
+`core/src/main/java/org/apache/struts2/result/ServletActionRedirectResult.java:135-156`):
+
+```java
+public ServletActionRedirectResult() //
no-arg
+
+public ServletActionRedirectResult(String actionName) // 1
String
+
+public ServletActionRedirectResult(String actionName, String method) // 2
Strings
+
+public ServletActionRedirectResult(String namespace, String actionName, String
method) // 3 Strings
+
+public ServletActionRedirectResult(String namespace, String actionName, String
method, String anchor) // 4 Strings
+```
+
+When Spring's constructor autowiring finds a String bean, it matches that bean
to **every** String parameter, resulting
+in:
+
+- `namespace = "XXXX"`
+- `actionName = "XXXX"`
+- `method = "XXXX"`
+- `anchor = "XXXX"`
+
+### Default Configuration
+
+In `core/src/main/resources/org/apache/struts2/default.properties`:
+
+```properties
+struts.objectFactory.spring.autoWire.alwaysRespect=false
+```
+
+This default value enables the legacy mixed injection strategy that causes the
issue.
+
+### Code Flow
+
+1. Struts needs to create a `ServletActionRedirectResult` instance
+2. `SpringObjectFactory.buildBean()` is called
+3. Since `alwaysRespectAutowireStrategy = false`, it uses constructor
autowiring:
+ ```java
+ bean = autoWiringFactory.autowire(clazz,
AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false);
+ ```
+4. Spring finds a String bean (the JNDI lookup) and injects it into
constructor String parameters
+5. The result is instantiated with incorrect values
+
+### Why the Workaround Works
+
+Setting `struts.objectFactory.spring.autoWire.alwaysRespect = true` causes the
code to take a different path (
+`SpringObjectFactory.java:188-192`):
+
+```java
+if(alwaysRespectAutowireStrategy){
+// Leave the creation up to Spring
+bean =autoWiringFactory.
+
+createBean(clazz, autowireStrategy, false);
+
+injectApplicationContext(bean);
+ return
+
+injectInternalBeans(bean);
+}
+```
+
+The default `autowireStrategy` is `AUTOWIRE_BY_NAME`, which only injects beans
when property names match bean names.
+Since `ServletActionRedirectResult` doesn't have properties named after JNDI
beans, no incorrect injection occurs.
+
+## Code References
+
+-
`plugins/spring/src/main/java/org/apache/struts2/spring/SpringObjectFactory.java:183-208`
- buildBean method with
+ mixed injection strategy
+-
`plugins/spring/src/main/java/org/apache/struts2/spring/SpringObjectFactory.java:58`
- alwaysRespectAutowireStrategy
+ field (default: false)
+-
`core/src/main/java/org/apache/struts2/result/ServletActionRedirectResult.java:135-156`
- Constructors with String
+ parameters
+- `core/src/main/java/org/apache/struts2/StrutsConstants.java:138` -
STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE_ALWAYS_RESPECT
+ constant
+- `core/src/main/resources/org/apache/struts2/default.properties` - Default
configuration
+
+## Test Coverage Gap
+
+There are **no tests** in the Spring plugin specifically testing
`ServletActionRedirectResult` autowiring behavior.
+Existing tests cover:
+
+- Constructor injection with custom beans
(`testShouldUseConstructorBasedInjectionWhenCreatingABeanFromAClassName`)
+- alwaysRespect configuration switching
+- Fallback behavior for ambiguous constructors
+
+But none test the scenario of a String bean being incorrectly injected into
result class constructors.
+
+## Potential Fixes
+
+1. **Change the default**: Set
`struts.objectFactory.spring.autoWire.alwaysRespect = true` as the default
+ - Pros: Fixes the issue for everyone
+ - Cons: Breaking change for existing applications relying on constructor
injection
+
+2. **Remove String-parameter constructors**: Use only the no-arg constructor
with setters
+ - Pros: Avoids the problem entirely
+ - Cons: Less convenient API, breaks existing code using these constructors
+
+3. **Add @Autowired(required=false) annotations**: Explicitly mark constructors
+ - Pros: Gives control over which constructors Spring considers
+ - Cons: Adds Spring-specific annotations to core classes
+
+4. **Use primary constructor designation**: Mark the no-arg constructor as
preferred
+ - Pros: Spring would prefer the no-arg constructor
+ - Cons: Requires additional Spring configuration
+
+## Workaround (Still Valid)
+
+Configure Struts to respect the autowire strategy:
+
+**struts.xml:**
+
+```xml
+
+<constant name="struts.objectFactory.spring.autoWire.alwaysRespect"
value="true"/>
+```
+
+**struts.properties:**
+
+```properties
+struts.objectFactory.spring.autoWire.alwaysRespect=true
+```
+
+## Architecture Insights
+
+The "mixed injection strategy" was designed to provide flexibility by:
+
+1. First using constructor autowiring to create beans
+2. Then applying property-based autowiring for additional dependencies
+
+However, this approach has an inherent flaw: constructor autowiring is too
aggressive with simple types like String,
+matching any available String bean to any String constructor parameter. This
design predates the modern understanding
+that constructor injection should primarily be used for required dependencies
with specific types.
+
+## Open Questions
+
+1. Should the default for `alwaysRespectAutowireStrategy` be changed to `true`?
+2. Are there other result classes or Struts components with similar
String-parameter constructors that might be
+ affected?
+3. Is the "mixed injection strategy" still needed, or should it be deprecated?