This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch 
feat/WW-5593-convention-plugin-noclass-exception
in repository https://gitbox.apache.org/repos/asf/struts.git

commit d4b2840434a28142e0acfa0ffe63f6b942ff8bea
Author: Lukasz Lenart <[email protected]>
AuthorDate: Tue Dec 2 18:49:26 2025 +0100

    docs(convention): WW-5593 add research for NoClassDefFoundError bug
    
    Add comprehensive research document analyzing the exception handling bug
    in PackageBasedActionConfigBuilder where NoClassDefFoundError is not caught,
    causing complete application failures instead of graceful degradation.
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude <[email protected]>
---
 ...-WW-5593-convention-plugin-noclass-exception.md | 385 +++++++++++++++++++++
 1 file changed, 385 insertions(+)

diff --git 
a/thoughts/shared/research/2025-12-02-WW-5593-convention-plugin-noclass-exception.md
 
b/thoughts/shared/research/2025-12-02-WW-5593-convention-plugin-noclass-exception.md
new file mode 100644
index 000000000..b4c7b4d14
--- /dev/null
+++ 
b/thoughts/shared/research/2025-12-02-WW-5593-convention-plugin-noclass-exception.md
@@ -0,0 +1,385 @@
+---
+date: 2025-12-02T18:33:08+01:00
+topic: "WW-5593: Convention plugin fails with NoClassDefFoundError"
+tags: [research, codebase, convention-plugin, exception-handling, 
bug-analysis, WW-5593]
+status: complete
+jira_ticket: WW-5593
+related_tickets: [WW-5594]
+---
+
+# Research: WW-5593 - Convention Plugin NoClassDefFoundError Handling
+
+**Date**: 2025-12-02T18:33:08+01:00
+**JIRA**: [WW-5593](https://issues.apache.org/jira/browse/WW-5593)
+**Related**: [WW-5594](https://issues.apache.org/jira/browse/WW-5594)
+
+## Research Question
+
+Why does the convention plugin fail completely with `NoClassDefFoundError` 
when scanning classes with missing dependencies, instead of gracefully logging 
and continuing?
+
+## Summary
+
+The `PackageBasedActionConfigBuilder` class (line 664) only catches 
`ClassNotFoundException` but not `NoClassDefFoundError` when testing action 
class candidates during classpath scanning. This causes complete application 
startup failures instead of graceful degradation when classes have missing 
optional dependencies.
+
+The issue is triggered when `struts.convention.action.includeJars` is 
configured, causing the plugin to scan struts2-core.jar which contains 
`XWorkTestCase` - a test utility class depending on JUnit (an optional 
dependency).
+
+## Detailed Findings
+
+### 1. The Exception Handling Bug
+
+**Location**: 
`plugins/convention/src/main/java/org/apache/struts2/convention/PackageBasedActionConfigBuilder.java:662-667`
+
+**Current Code**:
+```java
+try {
+    return inPackage && (nameMatches || (checkImplementsAction && 
org.apache.struts2.action.Action.class.isAssignableFrom(classInfo.get())));
+} catch (ClassNotFoundException ex) {
+    LOG.error("Unable to load class [{}]", classInfo.getName(), ex);
+    return false;
+}
+```
+
+**Problem**: Only catches `ClassNotFoundException`, missing 
`NoClassDefFoundError`.
+
+### 2. Exception Hierarchy
+
+```
+java.lang.Throwable
+  ├─ java.lang.Exception
+  │    └─ java.lang.ClassNotFoundException
+  └─ java.lang.Error
+       └─ java.lang.LinkageError
+            └─ java.lang.NoClassDefFoundError
+```
+
+**Key Point**: `NoClassDefFoundError` and `ClassNotFoundException` are 
siblings under `Throwable`, not parent-child. You cannot catch 
`NoClassDefFoundError` by catching `ClassNotFoundException`.
+
+**Difference**:
+- **ClassNotFoundException**: Thrown when `Class.forName()` or 
`ClassLoader.loadClass()` cannot find the class definition
+- **NoClassDefFoundError**: Thrown when a class was found at compile time but 
cannot be loaded at runtime due to:
+  - Missing dependencies (JAR files)
+  - Class initialization failures
+  - Static initializer blocks throw exceptions
+  - Incompatible class versions
+  - Classloader isolation issues
+
+### 3. Where NoClassDefFoundError Originates
+
+**Location**: 
`core/src/main/java/org/apache/struts2/util/finder/ClassFinder.java:224-235`
+
+```java
+public Class<?> get() throws ClassNotFoundException {
+    if (clazz != null) return clazz;
+    if (notFound != null) throw notFound;
+    try {
+        this.clazz = classFinder.getClassLoaderInterface().loadClass(name);
+        return clazz;
+    } catch (ClassNotFoundException notFound) {
+        classFinder.getClassesNotLoaded().add(name);
+        this.notFound = notFound;
+        throw notFound;
+    }
+}
+```
+
+**Analysis**:
+- The method signature declares `throws ClassNotFoundException`
+- The catch block only handles `ClassNotFoundException`
+- However, `ClassLoader.loadClass()` can also throw `NoClassDefFoundError` 
when class dependencies are missing
+- This `NoClassDefFoundError` propagates uncaught through `ClassInfo.get()`
+
+### 4. Call Stack During Failure
+
+From the email thread error log:
+
+```
+ERROR [org.apache.struts2.convention.DefaultClassFinder] (default task-1)
+Error loading class [org.apache.struts2.XWorkTestCase]:
+java.lang.NoClassDefFoundError: Failed to link org/apache/struts2/XWorkTestCase
+(Module "deployment.coreweb.war" from Service Module Loader): 
junit/framework/TestCase
+    at java.base/java.lang.ClassLoader.defineClass1(Native Method)
+    at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1017)
+    ...
+    at 
deployment.coreweb.war//org.apache.struts2.util.finder.ClassFinder$ClassInfo.get(ClassFinder.java:228)
+    at 
deployment.coreweb.war//org.apache.struts2.convention.PackageBasedActionConfigBuilder$1.test(PackageBasedActionConfigBuilder.java:6XX)
+    at 
deployment.coreweb.war//org.apache.struts2.convention.DefaultClassFinder.findClasses(DefaultClassFinder.java:280)
+```
+
+The error occurs when:
+1. Convention plugin scans for action classes
+2. Finds `org.apache.struts2.XWorkTestCase` as a candidate
+3. Calls `classInfo.get()` to load the class
+4. ClassLoader attempts to link the class and load `junit.framework.TestCase`
+5. JUnit is not available at runtime (optional dependency)
+6. `NoClassDefFoundError` is thrown
+7. **Not caught** by `PackageBasedActionConfigBuilder` catch block
+8. Application startup fails completely
+
+### 5. Correct Exception Handling Patterns in Struts
+
+The Struts codebase already uses correct patterns in other locations:
+
+#### Pattern A: Catch Throwable (Most Defensive)
+
+**Location**: 
`plugins/convention/src/main/java/org/apache/struts2/convention/DefaultClassFinder.java:283-286`
+
+```java
+} catch (Throwable e) {
+    LOG.error("Error loading class [{}]", classInfo.getName(), e);
+    classesNotLoaded.add(classInfo.getName());
+}
+```
+
+**Also used at**: Lines 248-251, 267-270, 297-300
+
+**Advantage**: Catches all exceptions and errors, including:
+- `ClassNotFoundException`
+- `NoClassDefFoundError`
+- `LinkageError`
+- `ExceptionInInitializerError`
+- Any other unexpected errors
+
+#### Pattern B: Multi-Catch with Specific Exceptions
+
+**Location**: 
`core/src/main/java/org/apache/struts2/config/providers/XmlDocConfigurationProvider.java:580-582`
+
+```java
+} catch (ClassNotFoundException | NoClassDefFoundError e) {
+    throw new ConfigurationException("Result class [" + className + "] not 
found", e, loc);
+}
+```
+
+**Location**: 
`core/src/main/java/org/apache/struts2/factory/DefaultInterceptorFactory.java:91-93`
+
+```java
+} catch (NoClassDefFoundError e) {
+    cause = e;
+    message = "Could not load class " + interceptorClassName + ". Perhaps it 
exists but certain dependencies are not available?";
+}
+```
+
+**Advantage**: Explicit about what exceptions are expected, provides better 
error messages
+
+#### Pattern C: Insufficient (Current Bug)
+
+**Location**: 
`plugins/convention/src/main/java/org/apache/struts2/convention/PackageBasedActionConfigBuilder.java:664`
+
+```java
+} catch (ClassNotFoundException ex) {
+    LOG.error("Unable to load class [{}]", classInfo.getName(), ex);
+    return false;
+}
+```
+
+**Problem**: Misses `NoClassDefFoundError` and other linkage errors
+
+### 6. When NoClassDefFoundError Occurs
+
+**Common Scenarios**:
+1. **Application redeployment** (as mentioned in related research)
+2. **Optional dependencies missing** (like JUnit for test classes)
+3. **Classloader isolation** in application servers (JBoss, WebSphere, etc.)
+4. **Static initializer failures** in class being loaded
+5. **Split package issues** in modular systems
+6. **Incompatible class versions** between dependencies
+
+### 7. Trigger Condition
+
+**From Email Thread**: Setting this constant triggers the bug:
+
+```xml
+<constant name="struts.convention.action.includeJars"
+          value=".*?/myjar.*?jar(!/)?" />
+```
+
+**Why**:
+- By default, convention plugin only scans application's own classes
+- Setting `struts.convention.action.includeJars` causes scanning of JAR files
+- This includes struts2-core.jar itself
+- struts2-core.jar contains `org.apache.struts2.XWorkTestCase`
+- `XWorkTestCase` depends on `junit.framework.TestCase`
+- JUnit is optional (scope: test)
+- At runtime, JUnit not available → `NoClassDefFoundError`
+
+## Code References
+
+- 
`plugins/convention/src/main/java/org/apache/struts2/convention/PackageBasedActionConfigBuilder.java:664`
 - Bug location
+- `core/src/main/java/org/apache/struts2/util/finder/ClassFinder.java:228` - 
Where NoClassDefFoundError originates
+- 
`plugins/convention/src/main/java/org/apache/struts2/convention/DefaultClassFinder.java:283-286`
 - Correct pattern (catches Throwable)
+- 
`core/src/main/java/org/apache/struts2/factory/DefaultInterceptorFactory.java:91-93`
 - Precedent for NoClassDefFoundError handling
+- 
`core/src/main/java/org/apache/struts2/config/providers/XmlDocConfigurationProvider.java:580-582`
 - Multi-catch pattern
+
+## Recommended Fix
+
+### Option A: Multi-Catch (Recommended)
+
+**File**: 
`plugins/convention/src/main/java/org/apache/struts2/convention/PackageBasedActionConfigBuilder.java:664`
+
+```java
+} catch (ClassNotFoundException | NoClassDefFoundError ex) {
+    LOG.error("Unable to load class [{}]. Perhaps it exists but certain 
dependencies are not available?",
+              classInfo.getName(), ex);
+    return false;
+}
+```
+
+**Advantages**:
+- Explicit about expected exceptions
+- Follows pattern from `XmlDocConfigurationProvider`
+- Provides helpful error message
+- Java 7+ multi-catch syntax
+
+### Option B: Catch Throwable (More Defensive)
+
+```java
+} catch (Throwable ex) {
+    LOG.error("Unable to load class [{}]", classInfo.getName(), ex);
+    return false;
+}
+```
+
+**Advantages**:
+- Matches pattern in `DefaultClassFinder`
+- Handles any unexpected errors
+- Most defensive approach
+
+**Disadvantages**:
+- Less specific
+- Could mask programming errors
+
+### Recommendation
+
+Use **Option A** (multi-catch) because:
+1. It's explicit about the expected error conditions
+2. Follows established pattern in `XmlDocConfigurationProvider`
+3. Provides better error message for users
+4. Still maintains good defensive programming
+
+### Additional Improvements
+
+Consider adding to the log message:
+```java
+LOG.error("Unable to load class [{}]. Perhaps it exists but certain 
dependencies are not available? " +
+          "If this is a test class or from a library, consider adding it to 
struts.convention.exclude.packages",
+          classInfo.getName(), ex);
+```
+
+## Impact Assessment
+
+### Current Risk
+
+**Severity**: HIGH
+
+**Scenarios Affected**:
+1. Applications using `struts.convention.action.includeJars`
+2. Applications with optional dependencies on scanned classes
+3. Redeployment scenarios with classloader issues
+4. Application server environments with module isolation (JBoss, WebSphere)
+
+**Current Behavior**:
+- ❌ Complete application startup failure
+- ❌ No graceful degradation
+- ❌ Misleading error (appears critical, not missing optional dependency)
+- ❌ Users must add workarounds to proceed
+
+### After Fix
+
+**Expected Behavior**:
+- ✅ Graceful degradation - problematic classes logged and skipped
+- ✅ Application continues to function
+- ✅ Only unavailable actions are affected, not entire app
+- ✅ Clear error messages indicating dependency issues
+- ✅ Consistent with `DefaultClassFinder` behavior
+
+## Testing Strategy
+
+### Unit Tests to Add
+
+1. **Test NoClassDefFoundError Handling**:
+```java
+@Test
+public void testHandlesNoClassDefFoundError() {
+    // Mock a ClassInfo that throws NoClassDefFoundError
+    ClassInfo classInfo = mock(ClassInfo.class);
+    when(classInfo.get()).thenThrow(new 
NoClassDefFoundError("junit/framework/TestCase"));
+
+    // Should return false (exclude class) instead of propagating error
+    
assertFalse(builder.includeClassNameInActionScan("org.apache.struts2.XWorkTestCase"));
+
+    // Should log error
+    verify(mockLogger).error(contains("Unable to load class"), 
any(NoClassDefFoundError.class));
+}
+```
+
+2. **Test ClassNotFoundException Handling** (existing behavior):
+```java
+@Test
+public void testHandlesClassNotFoundException() {
+    ClassInfo classInfo = mock(ClassInfo.class);
+    when(classInfo.get()).thenThrow(new 
ClassNotFoundException("com.example.MissingClass"));
+
+    
assertFalse(builder.includeClassNameInActionScan("com.example.MissingClass"));
+    verify(mockLogger).error(contains("Unable to load class"), 
any(ClassNotFoundException.class));
+}
+```
+
+3. **Test ExceptionInInitializerError** (edge case):
+```java
+@Test
+public void testHandlesExceptionInInitializerError() {
+    ClassInfo classInfo = mock(ClassInfo.class);
+    when(classInfo.get()).thenThrow(new ExceptionInInitializerError("Static 
init failed"));
+
+    
assertFalse(builder.includeClassNameInActionScan("com.example.BadStaticInit"));
+}
+```
+
+### Integration Tests
+
+1. Create a test JAR with a class that has missing dependencies
+2. Configure `struts.convention.action.includeJars` to scan that JAR
+3. Verify application starts successfully despite the problematic class
+4. Verify error is logged appropriately
+
+## Workaround for Users
+
+Until the fix is released, users can:
+
+**Option 1**: Exclude problematic packages:
+```xml
+<constant name="struts.convention.exclude.packages"
+          value="org.apache.struts2,org.apache.struts2.*" />
+```
+
+**Option 2**: Include JUnit at runtime (not recommended):
+```xml
+<dependency>
+    <groupId>junit</groupId>
+    <artifactId>junit</artifactId>
+    <version>4.13.2</version>
+    <scope>runtime</scope>
+</dependency>
+```
+
+**Option 3**: Don't use `struts.convention.action.includeJars` if not needed
+
+## Related Issues
+
+- **WW-5594**: Wildcard pattern matching issue preventing proper exclusion of 
org.apache.struts2 package
+- Related research: 
`thoughts/shared/research/2025-12-02-memory-leak-redeployment.md` (redeployment 
issues)
+
+## Email Thread Reference
+
+**From**: Florian Schlittgen ([email protected])
+**Date**: December 19, 2024 - January 19, 2025
+**Subject**: Struts 7: action class finder
+**List**: [email protected]
+
+## Next Steps
+
+1. ✅ Create JIRA ticket WW-5593
+2. ⏳ Implement fix in `PackageBasedActionConfigBuilder.java:664`
+3. ⏳ Add unit tests for NoClassDefFoundError handling
+4. ⏳ Add integration tests with missing dependencies
+5. ⏳ Update any other catch blocks with same issue
+6. ⏳ Review release notes to document the fix
\ No newline at end of file

Reply via email to