Fix MethodUtils.invokeMethod on instances of non-public classes - #1783
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The new JDK List.size() assertions expect List.class as the declaring type, but the public declaration typically resolves to java.util.Collection, making the test incorrect/brittle.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR fixes MethodUtils.getMatchingAccessibleMethod’s exact-signature fast path so that when the receiver is a non-public class (common for JDK factory collections), the returned Method is re-resolved to a publicly accessible declaration (e.g., via an interface) to avoid IllegalAccessException during invokeMethod.
Changes:
- Update
getMatchingAccessibleMethodfast path to prefergetAccessibleMethod(cls, candidate)when available. - Add new unit tests covering non-public receiver classes (JDK collections and package-private test classes) and the
invokeMethodregression scenario.
File summaries
| File | Description |
|---|---|
| src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java | Re-resolves exact-match hits through getAccessibleMethod to avoid returning inaccessible declarations from non-public receiver classes. |
| src/test/java/org/apache/commons/lang3/reflect/MethodUtilsTest.java | Adds tests that exercise accessible-method resolution and invocation on non-public receiver classes (including JDK collection factories). |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| assertSame(List.class, | ||
| MethodUtils.getMatchingAccessibleMethod(Collections.emptyList().getClass(), "size").getDeclaringClass()); | ||
| assertSame(List.class, | ||
| MethodUtils.getMatchingAccessibleMethod(Arrays.asList(1, 2).getClass(), "size").getDeclaringClass()); |
There was a problem hiding this comment.
java.util.List declares size() itself, so the interface walk in getAccessibleMethodFromInterfaceNest stops at List before it reaches Collection, and Map declares its own size() the same way. The assertions hold on every JDK in the matrix, 8 through 27.
|
I think this PR creates 2 bugs so it looks like we are missing some tests since the build was green, so please add these missing tests and update the main code:
Note: Your AI is imagining things when it talks about PR #1427 because that PR was closed without being merged. |
… static interface method is not a stand-in
|
Both cases reproduced on the first commit and are fixed in efc8e2e: a static candidate is kept as is, since a static method hides rather than overrides, and the interface walk skips static interface methods, which are not inherited. Tests for both use your examples ( |
|
Here's another wrinkle: On Java 9 and up, interfaces can contain private instance methods, which are not inherited and cannot serve as accessible replacements. So you need to exclude private interface methods as well as static methods. Since our code is Java 8-based, the only way to test this is to add a precompiled Java code to the test resource folder. For example: public interface Labels {
private String label() { return "helper"; }
}
static class Bean implements Labels {
public String label() { return "bean"; }
}For
|
A private interface method is not inherited, so it cannot stand in for the implementing class's own method.
|
Pushed the private interface method case. getAccessibleMethodFromInterfaceNest now returns a declared interface method only when it is public and not static, so a private one falls through to the class's own public method. Without that change the new test errors with IllegalAccessException on your example, and it passes with it. The fixture pair is compiled with javac release 9 and checked in under src/test/resources next to its sources, since test sources build at 1.8. The test assumes Java 9 or later, so the Java 8 jobs skip it. Default goal green locally with -Ddoclint=all. The workflow runs on this head are action_required. |
|
Thank you @lenamonj , merged 🚀 |
getMatchingAccessibleMethodhas an exact-signature fast path, added on master in ee09f69, that returns theMethoddeclared on the receiver's class without the accessible-declaration lookup the search path does. When that class is not public, as with every JDK collection factory result,invokeMethodthrowsIllegalAccessExceptionalthough a public declaration exists on the interface.The fast path now re-resolves an instance hit through
getAccessibleMethodand keeps the original when no public declaration exists. A static hit is kept as is, since a static method hides rather than overrides; the interface walk skips static interface methods, which are not inherited.Tests: the first four are red on master, the two static cases were red on the first commit. Default Maven goal green.
mvn; that'smvnon the command line by itself.