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

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git


The following commit(s) were added to refs/heads/master by this push:
     new d8f4116d4 Fix MethodUtils.invokeMethod on instances of non-public 
classes (#1783)
d8f4116d4 is described below

commit d8f4116d4ba03ed32b104b3af891de7a67f2ce97
Author: Jeff Lenamon <[email protected]>
AuthorDate: Wed Sep 9 02:33:04 2026 -0400

    Fix MethodUtils.invokeMethod on instances of non-public classes (#1783)
    
    * Fix MethodUtils.invokeMethod on instances of non-public classes
    
    * Keep static declarations: a static candidate is not re-resolved and a 
static interface method is not a stand-in
    
    * Return only a public instance method from the interface nest search
    
    A private interface method is not inherited, so it cannot stand in for the 
implementing class's own method.
---
 .../apache/commons/lang3/reflect/MethodUtils.java  |  12 ++-
 .../commons/lang3/reflect/MethodUtilsTest.java     |  86 +++++++++++++++++++++
 .../reflect/testbed9/PrivateInterfaceBean.class    | Bin 0 -> 410 bytes
 .../reflect/testbed9/PrivateInterfaceBean.java     |  24 ++++++
 .../reflect/testbed9/PrivateInterfaceLabels.class  | Bin 0 -> 448 bytes
 .../reflect/testbed9/PrivateInterfaceLabels.java   |  32 ++++++++
 6 files changed, 151 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java 
b/src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java
index 03fa977ec..657d2e06f 100644
--- a/src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java
+++ b/src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java
@@ -179,9 +179,12 @@ private static Method 
getAccessibleMethodFromInterfaceNest(Class<?> cls, final S
                 if (!ClassUtils.isPublic(anInterface)) {
                     continue;
                 }
-                // Does the method exist on this interface?
+                // Does the method exist on this interface? A static or 
private one is not inherited.
                 try {
-                    return anInterface.getDeclaredMethod(methodName, 
parameterTypes);
+                    final Method declared = 
anInterface.getDeclaredMethod(methodName, parameterTypes);
+                    if (MemberUtils.isPublic(declared) && 
!MemberUtils.isStatic(declared)) {
+                        return declared;
+                    }
                 } catch (final NoSuchMethodException ignored) {
                     /*
                      * Swallow, if no method is found after the loop then this 
method returns null.
@@ -369,7 +372,10 @@ private static Method getInvokeMethod(final boolean 
forceAccess, final String me
     public static Method getMatchingAccessibleMethod(final Class<?> cls, final 
String methodName, final Class<?>... requestTypes) {
         final Method candidate = getMethodObject(cls, methodName, 
requestTypes);
         if (candidate != null) {
-            return MemberUtils.setAccessibleWorkaround(candidate);
+            // The exact match may be declared on a non-public class, so 
prefer the public
+            // declaration the way the search below does; a static method 
hides, so it is kept.
+            final Method accessibleCandidate = MemberUtils.isStatic(candidate) 
? null : getAccessibleMethod(cls, candidate);
+            return MemberUtils.setAccessibleWorkaround(accessibleCandidate != 
null ? accessibleCandidate : candidate);
         }
         // search through all methods
         final Method[] methods = cls.getMethods();
diff --git 
a/src/test/java/org/apache/commons/lang3/reflect/MethodUtilsTest.java 
b/src/test/java/org/apache/commons/lang3/reflect/MethodUtilsTest.java
index 111054ed3..475413286 100644
--- a/src/test/java/org/apache/commons/lang3/reflect/MethodUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/reflect/MethodUtilsTest.java
@@ -26,6 +26,7 @@
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
 
 import java.awt.Color;
 import java.lang.reflect.Method;
@@ -34,7 +35,9 @@
 import java.nio.file.Files;
 import java.nio.file.LinkOption;
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -45,6 +48,8 @@
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.ClassUtils;
 import org.apache.commons.lang3.ClassUtils.Interfaces;
+import org.apache.commons.lang3.JavaVersion;
+import org.apache.commons.lang3.SystemUtils;
 import org.apache.commons.lang3.math.NumberUtils;
 import org.apache.commons.lang3.mutable.Mutable;
 import org.apache.commons.lang3.mutable.MutableObject;
@@ -523,6 +528,30 @@ public String foo() {
         }
     }
 
+    public static class StaticParent {
+        public static String who() {
+            return "parent";
+        }
+    }
+
+    static class StaticChild extends StaticParent {
+        public static String who() {
+            return "child";
+        }
+    }
+
+    public interface StaticLabel {
+        static String label() {
+            return "interface";
+        }
+    }
+
+    static class InstanceLabel implements StaticLabel {
+        public String label() {
+            return "instance";
+        }
+    }
+
     private static class TestMutable implements Mutable<Object> {
         @Override
         public Object getValue() {
@@ -605,6 +634,63 @@ void testGetAccessibleInterfaceMethodFromDescription(final 
Class<?> clazz) {
         }
     }
 
+    @ParameterizedTest
+    @ValueSource(classes = {TestMutable.class, TestMutableSubclass.class})
+    void testGetMatchingAccessibleMethodOnNonPublicClass(final Class<?> clazz) 
{
+        assertSame(Mutable.class, 
MethodUtils.getMatchingAccessibleMethod(clazz, "getValue").getDeclaringClass());
+        assertSame(Mutable.class,
+                MethodUtils.getMatchingAccessibleMethod(clazz, "setValue", 
Object.class).getDeclaringClass());
+    }
+
+    @Test
+    void testGetMatchingAccessibleMethodOnNonPublicJdkClass() {
+        assertSame(List.class,
+                
MethodUtils.getMatchingAccessibleMethod(Collections.emptyList().getClass(), 
"size").getDeclaringClass());
+        assertSame(List.class,
+                MethodUtils.getMatchingAccessibleMethod(Arrays.asList(1, 
2).getClass(), "size").getDeclaringClass());
+        assertSame(Map.class,
+                
MethodUtils.getMatchingAccessibleMethod(Collections.emptyMap().getClass(), 
"size").getDeclaringClass());
+    }
+
+    @Test
+    void testGetMatchingAccessibleMethodWithNoPublicDeclaration() {
+        assertSame(TestBeanWithInterfaces.class,
+                
MethodUtils.getMatchingAccessibleMethod(TestBeanWithInterfaces.class, 
"foo").getDeclaringClass());
+    }
+
+    @Test
+    void testInvokeMethodOnNonPublicClass() throws Exception {
+        assertEquals(0, MethodUtils.invokeMethod(Collections.emptyList(), 
"size"));
+        assertEquals(2, MethodUtils.invokeMethod(Arrays.asList(1, 2), "size"));
+        assertEquals(0, MethodUtils.invokeMethod(Collections.emptyMap(), 
"size"));
+        assertEquals(0, 
MethodUtils.invokeMethod(Collections.unmodifiableList(new ArrayList<>()), 
"size"));
+        assertNull(MethodUtils.invokeMethod(new TestMutable(), "getValue"));
+    }
+
+    @Test
+    void testInvokeStaticMethodOnNonPublicSubclass() throws Exception {
+        // A static method hides rather than overrides, so the subclass's own 
declaration is invoked.
+        assertSame(StaticChild.class, 
MethodUtils.getMatchingAccessibleMethod(StaticChild.class, 
"who").getDeclaringClass());
+        assertEquals("child", 
MethodUtils.invokeStaticMethod(StaticChild.class, "who"));
+    }
+
+    @Test
+    void testInvokeMethodIgnoresStaticInterfaceMethod() throws Exception {
+        assertSame(InstanceLabel.class, 
MethodUtils.getMatchingAccessibleMethod(InstanceLabel.class, 
"label").getDeclaringClass());
+        assertEquals("instance", MethodUtils.invokeMethod(new InstanceLabel(), 
"label"));
+        assertNull(MethodUtils.getAccessibleMethod(InstanceLabel.class, 
"label"));
+    }
+
+    @Test
+    void testInvokeMethodIgnoresPrivateInterfaceMethod() throws Exception {
+        // A private interface method needs Java 9, so the pair is precompiled 
under src/test/resources.
+        assumeTrue(SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_9));
+        final Class<?> labels = 
Class.forName("org.apache.commons.lang3.reflect.testbed9.PrivateInterfaceLabels");
+        final Object bean = labels.getMethod("newBean").invoke(null);
+        assertEquals("bean", MethodUtils.invokeMethod(bean, "label"));
+        assertNull(MethodUtils.getAccessibleMethod(bean.getClass(), "label"));
+    }
+
     @Test
     void testGetAccessibleMethodInaccessible() throws Exception {
         
assertNull(MethodUtils.getAccessibleMethod(TestBean.class.getDeclaredMethod("privateStuff")));
diff --git 
a/src/test/resources/org/apache/commons/lang3/reflect/testbed9/PrivateInterfaceBean.class
 
b/src/test/resources/org/apache/commons/lang3/reflect/testbed9/PrivateInterfaceBean.class
new file mode 100644
index 000000000..644945307
Binary files /dev/null and 
b/src/test/resources/org/apache/commons/lang3/reflect/testbed9/PrivateInterfaceBean.class
 differ
diff --git 
a/src/test/resources/org/apache/commons/lang3/reflect/testbed9/PrivateInterfaceBean.java
 
b/src/test/resources/org/apache/commons/lang3/reflect/testbed9/PrivateInterfaceBean.java
new file mode 100644
index 000000000..6f87ac516
--- /dev/null
+++ 
b/src/test/resources/org/apache/commons/lang3/reflect/testbed9/PrivateInterfaceBean.java
@@ -0,0 +1,24 @@
+/*
+ * 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
+ *
+ *      https://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 org.apache.commons.lang3.reflect.testbed9;
+
+class PrivateInterfaceBean implements PrivateInterfaceLabels {
+
+    public String label() {
+        return "bean";
+    }
+}
diff --git 
a/src/test/resources/org/apache/commons/lang3/reflect/testbed9/PrivateInterfaceLabels.class
 
b/src/test/resources/org/apache/commons/lang3/reflect/testbed9/PrivateInterfaceLabels.class
new file mode 100644
index 000000000..1735ced28
Binary files /dev/null and 
b/src/test/resources/org/apache/commons/lang3/reflect/testbed9/PrivateInterfaceLabels.class
 differ
diff --git 
a/src/test/resources/org/apache/commons/lang3/reflect/testbed9/PrivateInterfaceLabels.java
 
b/src/test/resources/org/apache/commons/lang3/reflect/testbed9/PrivateInterfaceLabels.java
new file mode 100644
index 000000000..58a669578
--- /dev/null
+++ 
b/src/test/resources/org/apache/commons/lang3/reflect/testbed9/PrivateInterfaceLabels.java
@@ -0,0 +1,32 @@
+/*
+ * 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
+ *
+ *      https://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 org.apache.commons.lang3.reflect.testbed9;
+
+/**
+ * A private interface method needs Java 9, so this package is compiled with 
javac --release 9 and shipped as class files
+ * next to this source; the test sources are Java 8.
+ */
+public interface PrivateInterfaceLabels {
+
+    static Object newBean() {
+        return new PrivateInterfaceBean();
+    }
+
+    private String label() {
+        return "helper";
+    }
+}

Reply via email to