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";
+ }
+}