Skip to content

Commit 1d5ba7a

Browse files
committed
MethodUtils cannot find or invoke a public method on a public class
implemented in its package-private superclass - Add org.apache.commons.lang3.reflect.MethodUtils.getAccessibleMethod(Class<?>, Method)
1 parent 4c39aa2 commit 1d5ba7a

File tree

5 files changed

+182
-32
lines changed

5 files changed

+182
-32
lines changed

src/changes/changes.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ The <action> type attribute can be add,update,fix,remove.
4949
<action issue="LANG-1778" type="fix" dev="ggregory" due-to="wuwu2000">MethodUtils.getMatchingMethod() doesn't respect the hierarchy of methods #1414.</action>
5050
<action type="fix" dev="ggregory" due-to="Gary Gregory">org.apache.commons.lang3.reflect.MethodUtils.getMethodObject(Class&lt;?&gt;, String, Class&lt;?&gt;...) now returns null instead of throwing a NullPointerException, as it does for other exception types.</action>
5151
<action type="fix" dev="ggregory" due-to="Gary Gregory">Reduce spurious failures in org.apache.commons.lang3.ArrayUtilsTest methods that test ArrayUtils.shuffle() methods.</action>
52+
<action type="fix" dev="ggregory" due-to="Gary Gregory">MethodUtils cannot find or invoke a public method on a public class implemented in its package-private superclass.</action>
5253
<!-- FIX Javadoc -->
5354
<action type="fix" dev="ggregory" due-to="Gary Gregory">[javadoc] General improvements.</action>
5455
<action type="fix" dev="ggregory" due-to="Gary Gregory">[javadoc] Fix thrown exception documentation for org.apache.commons.lang3.reflect.MethodUtils.getMethodObject(Class&lt;?&gt;, String, Class&lt;?&gt;...).</action>
@@ -61,6 +62,7 @@ The <action> type attribute can be add,update,fix,remove.
6162
<!-- ADD -->
6263
<action type="add" dev="ggregory" due-to="Gary Gregory">Add org.apache.commons.lang3.ArrayUtils.SOFT_MAX_ARRAY_LENGTH.</action>
6364
<action type="add" dev="ggregory" due-to="Gary Gregory">Add org.apache.commons.lang3.SystemUtils.IS_OS_NETWARE.</action>
65+
<action type="add" dev="ggregory" due-to="Gary Gregory">Add org.apache.commons.lang3.reflect.MethodUtils.getAccessibleMethod(Class, Method).</action>
6466
<!-- UPDATE -->
6567
<action type="update" dev="ggregory" due-to="Gary Gregory">[test] Bump org.apache.commons:commons-text from 1.13.1 to 1.14.0.</action>
6668
</release>

src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -109,31 +109,39 @@ public static Method getAccessibleMethod(final Class<?> cls, final String method
109109
}
110110

111111
/**
112-
* Gets an accessible method (that is, one that can be invoked via
113-
* reflection) that implements the specified Method. If no such method
114-
* can be found, return {@code null}.
112+
* Gets an accessible method (that is, one that can be invoked via reflection) that implements the specified Method. If no such method can be found, return
113+
* {@code null}.
115114
*
116115
* @param method The method that we wish to call, may be null.
117116
* @return The accessible method
118117
*/
119-
public static Method getAccessibleMethod(Method method) {
120-
if (!MemberUtils.isAccessible(method)) {
118+
public static Method getAccessibleMethod(final Method method) {
119+
return method != null ? getAccessibleMethod(method.getDeclaringClass(), method) : null;
120+
}
121+
122+
/**
123+
* Gets an accessible method (that is, one that can be invoked via reflection) that implements the specified Method. If no such method can be found, return
124+
* {@code null}.
125+
*
126+
* @param cls The implementing class, may be null.
127+
* @param method The method that we wish to call, may be null.
128+
* @return The accessible method or null.
129+
* @since 3.19.0
130+
*/
131+
public static Method getAccessibleMethod(final Class<?> cls, final Method method) {
132+
if (!MemberUtils.isPublic(method)) {
121133
return null;
122134
}
123135
// If the declaring class is public, we are done
124-
final Class<?> cls = method.getDeclaringClass();
125136
if (ClassUtils.isPublic(cls)) {
126137
return method;
127138
}
128139
final String methodName = method.getName();
129140
final Class<?>[] parameterTypes = method.getParameterTypes();
130141
// Check the implemented interfaces and subinterfaces
131-
method = getAccessibleMethodFromInterfaceNest(cls, methodName, parameterTypes);
142+
final Method method2 = getAccessibleMethodFromInterfaceNest(cls, methodName, parameterTypes);
132143
// Check the superclass chain
133-
if (method == null) {
134-
method = getAccessibleMethodFromSuperclass(cls, methodName, parameterTypes);
135-
}
136-
return method;
144+
return method2 != null ? method2 : getAccessibleMethodFromSuperclass(cls, methodName, parameterTypes);
137145
}
138146

139147
/**

src/test/java/org/apache/commons/lang3/reflect/MethodUtilsTest.java

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
import java.awt.Color;
3131
import java.lang.reflect.Method;
32+
import java.lang.reflect.Modifier;
3233
import java.lang.reflect.Type;
3334
import java.nio.file.Files;
3435
import java.nio.file.LinkOption;
@@ -55,19 +56,22 @@
5556
import org.apache.commons.lang3.tuple.ImmutablePair;
5657
import org.junit.jupiter.api.BeforeEach;
5758
import org.junit.jupiter.api.Test;
59+
import org.junit.jupiter.params.ParameterizedTest;
60+
import org.junit.jupiter.params.provider.ValueSource;
5861

5962
/**
6063
* {@link Tests MethodUtils}.
6164
*/
6265
class MethodUtilsTest extends AbstractLangTest {
66+
6367
protected abstract static class AbstractGetMatchingMethod implements InterfaceGetMatchingMethod {
6468
public abstract void testMethod5(Exception exception);
6569
}
70+
6671
protected abstract static class AbstractGetMatchingMethod2 implements InterfaceGetMatchingMethod {
6772
@Override
6873
public void testMethod6() { }
6974
}
70-
7175
interface ChildInterface {
7276
}
7377

@@ -116,14 +120,15 @@ public void testMethod4(final Color aColor1, final Color aColor2) {
116120
public void testMethod4(final Long aLong, final Long anotherLong) {
117121
}
118122
}
123+
119124
private static final class GetMatchingMethodImpl extends AbstractGetMatchingMethod {
120125
@Override
121126
public void testMethod5(final Exception exception) {
122127
}
123128
}
124-
125129
public static class GrandParentObject {
126130
}
131+
127132
public static class InheritanceBean {
128133
public void testOne(final GrandParentObject obj) {
129134
}
@@ -143,7 +148,6 @@ public void testTwo(final GrandParentObject obj) {
143148
public void testTwo(final Object obj) {
144149
}
145150
}
146-
147151
interface InterfaceGetMatchingMethod {
148152
default void testMethod6() {
149153
}
@@ -403,13 +407,16 @@ public ImmutablePair<String, Object[]> varOverloadEcho(final String... args) {
403407

404408
}
405409

410+
static class TestBeanSubclass extends TestBean {
411+
}
412+
406413
static class TestBeanWithInterfaces implements PrivateInterface {
407414
public String foo() {
408415
return "foo()";
409416
}
410417
}
411418

412-
private static final class TestMutable implements Mutable<Object> {
419+
private static class TestMutable implements Mutable<Object> {
413420
@Override
414421
public Object getValue() {
415422
return null;
@@ -420,6 +427,10 @@ public void setValue(final Object value) {
420427
}
421428
}
422429

430+
private static final class TestMutableSubclass extends TestMutable {
431+
432+
}
433+
423434
private TestBean testBean;
424435

425436
private final Map<Class<?>, Class<?>[]> classCache = new HashMap<>();
@@ -466,32 +477,36 @@ void testDistance() throws Exception {
466477
distanceMethod.setAccessible(false);
467478
}
468479

469-
@Test
470-
void testGetAccessibleInterfaceMethod() throws Exception {
480+
@ParameterizedTest
481+
@ValueSource(classes = {TestMutable.class, TestMutableSubclass.class})
482+
void testGetAccessibleInterfaceMethod(final Class<?> clazz) throws Exception {
471483
final Class<?>[][] p = {ArrayUtils.EMPTY_CLASS_ARRAY, null};
472484
for (final Class<?>[] element : p) {
473-
final Method method = TestMutable.class.getMethod("getValue", element);
485+
final Method method = clazz.getMethod("getValue", element);
474486
final Method accessibleMethod = MethodUtils.getAccessibleMethod(method);
475487
assertNotSame(accessibleMethod, method);
476488
assertSame(Mutable.class, accessibleMethod.getDeclaringClass());
489+
final Method accessibleMethod2 = MethodUtils.getAccessibleMethod(clazz, method);
490+
assertNotSame(accessibleMethod2, method);
491+
assertSame(Mutable.class, accessibleMethod2.getDeclaringClass());
477492
}
478493
}
479494

480-
@Test
481-
void testGetAccessibleInterfaceMethodFromDescription() {
482-
final Class<?>[][] p = {ArrayUtils.EMPTY_CLASS_ARRAY, null};
495+
@ParameterizedTest
496+
@ValueSource(classes = {TestMutable.class, TestMutableSubclass.class})
497+
void testGetAccessibleInterfaceMethodFromDescription(final Class<?> clazz) {
498+
final Class<?>[][] p = { ArrayUtils.EMPTY_CLASS_ARRAY, null };
483499
for (final Class<?>[] element : p) {
484-
final Method accessibleMethod = MethodUtils.getAccessibleMethod(
485-
TestMutable.class, "getValue", element);
500+
final Method accessibleMethod = MethodUtils.getAccessibleMethod(clazz, "getValue", element);
486501
assertSame(Mutable.class, accessibleMethod.getDeclaringClass());
487502
}
488503
}
489504

490505
@Test
491506
void testGetAccessibleMethodInaccessible() throws Exception {
492-
final Method expected = TestBean.class.getDeclaredMethod("privateStuff");
493-
final Method actual = MethodUtils.getAccessibleMethod(expected);
494-
assertNull(actual);
507+
assertNull(MethodUtils.getAccessibleMethod(TestBean.class.getDeclaredMethod("privateStuff")));
508+
assertNull(MethodUtils.getAccessibleMethod(TestBean.class, TestBean.class.getDeclaredMethod("privateStuff")));
509+
assertNull(MethodUtils.getAccessibleMethod(TestBeanSubclass.class, TestBean.class.getDeclaredMethod("privateStuff")));
495510
}
496511

497512
@Test
@@ -502,18 +517,52 @@ void testGetAccessibleMethodPrivateInterface() throws Exception {
502517
assertNull(actual);
503518
}
504519

520+
@Test
521+
void testGetAccessibleMethodPublicSub() throws Exception {
522+
// PackageBean class is package-private
523+
final int modifiers = PackageBean.class.getModifiers();
524+
assertFalse(Modifier.isPrivate(modifiers));
525+
assertFalse(Modifier.isProtected(modifiers));
526+
assertFalse(Modifier.isPublic(modifiers));
527+
// make sure that bean does what it should: compile
528+
new PublicSubBean().setBar("");
529+
// make sure that bean does what it should
530+
final PublicSubBean bean = new PublicSubBean();
531+
assertEquals(bean.getFoo(), "This is foo", "Start value (foo)");
532+
assertEquals(bean.getBar(), "This is bar", "Start value (bar)");
533+
bean.setFoo("new foo");
534+
bean.setBar("new bar");
535+
assertEquals(bean.getFoo(), "new foo", "Set value (foo)");
536+
assertEquals(bean.getBar(), "new bar", "Set value (bar)");
537+
// see if we can access public methods in a default access superclass
538+
// from a public access subclass instance
539+
MethodUtils.invokeExactMethod(bean, "setFoo", "alpha");
540+
assertEquals(bean.getFoo(), "alpha", "Set value (foo:2)");
541+
MethodUtils.invokeExactMethod(bean, "setBar", "beta");
542+
assertEquals(bean.getBar(), "beta", "Set value (bar:2)");
543+
// PublicSubBean.setFoo(String)
544+
Method method = MethodUtils.getAccessibleMethod(PublicSubBean.class, "setFoo", String.class);
545+
assertNotNull(method, "getAccessibleMethod() setFoo is Null");
546+
method.invoke(bean, "1111");
547+
assertEquals("1111", bean.getFoo(), "Set value (foo:3)");
548+
// PublicSubBean.setBar(String)
549+
method = MethodUtils.getAccessibleMethod(PublicSubBean.class, "setBar", String.class);
550+
assertNotNull(method, "getAccessibleMethod() setBar is Null");
551+
method.invoke(bean, "2222");
552+
assertEquals("2222", bean.getBar(), "Set value (bar:3)");
553+
}
554+
505555
@Test
506556
void testGetAccessiblePublicMethod() throws Exception {
507-
assertSame(MutableObject.class, MethodUtils.getAccessibleMethod(
508-
MutableObject.class.getMethod("getValue",
509-
ArrayUtils.EMPTY_CLASS_ARRAY)).getDeclaringClass());
557+
assertSame(MutableObject.class,
558+
MethodUtils.getAccessibleMethod(MutableObject.class.getMethod("getValue", ArrayUtils.EMPTY_CLASS_ARRAY)).getDeclaringClass());
559+
assertSame(MutableObject.class, MethodUtils
560+
.getAccessibleMethod(MutableObject.class, MutableObject.class.getMethod("getValue", ArrayUtils.EMPTY_CLASS_ARRAY)).getDeclaringClass());
510561
}
511562

512563
@Test
513564
void testGetAccessiblePublicMethodFromDescription() {
514-
assertSame(MutableObject.class, MethodUtils.getAccessibleMethod(
515-
MutableObject.class, "getValue", ArrayUtils.EMPTY_CLASS_ARRAY)
516-
.getDeclaringClass());
565+
assertSame(MutableObject.class, MethodUtils.getAccessibleMethod(MutableObject.class, "getValue", ArrayUtils.EMPTY_CLASS_ARRAY).getDeclaringClass());
517566
}
518567

519568
@Test
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.commons.lang3.reflect;
19+
20+
/**
21+
* This class is designed to test the default access JVM problem workaround. The issue is that public methods of a public subclass contained in a default access
22+
* superclass are returned by reflection but an IllegalAccessException is thrown when they are invoked.
23+
* <p>
24+
* This is the default access superclass
25+
* </p>
26+
*/
27+
class PackageBean {
28+
29+
private String bar = "This is bar";
30+
31+
/**
32+
* Package private constructor, can only use factory method to create beans.
33+
*/
34+
PackageBean() {
35+
}
36+
37+
public String getBar() {
38+
return this.bar;
39+
}
40+
41+
public void setBar(final String bar) {
42+
this.bar = bar;
43+
}
44+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.commons.lang3.reflect;
19+
20+
/**
21+
* This class is designed to test the default access JVM problem workaround. The issue is that public methods of a public subclass contained in a default access
22+
* superclass are returned by reflection but an IllegalAccessException is thrown when they are invoked.
23+
* <p>
24+
* This is the default access superclass
25+
* </p>
26+
*/
27+
public class PublicSubBean extends PackageBean {
28+
29+
/**
30+
* A directly implemented property.
31+
*/
32+
private String foo = "This is foo";
33+
34+
/**
35+
* Package private constructor, can only use factory method to create beans.
36+
*/
37+
public PublicSubBean() {
38+
}
39+
40+
public String getFoo() {
41+
return this.foo;
42+
}
43+
44+
public void setFoo(final String foo) {
45+
this.foo = foo;
46+
}
47+
}

0 commit comments

Comments
 (0)