Skip to content

Commit b1f5b61

Browse files
committed
Abort search for static methods in getPubliclyAccessibleMethodIfPossible()
Prior to this commit, getPubliclyAccessibleMethodIfPossible() in ClassUtils incorrectly returned a hidden static method as an "equivalent" method for a static method with the same signature; however, a static method cannot be overridden and therefore has no "equivalent" method in a super type. To fix that bug, this commit immediately aborts the search for an "equivalent" publicly accessible method when the original method is a static method. See gh-33216 See gh-35189 See gh-35556 Closes gh-35667
1 parent 810e069 commit b1f5b61

File tree

3 files changed

+90
-13
lines changed

3 files changed

+90
-13
lines changed

spring-core/src/main/java/org/springframework/util/ClassUtils.java

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1483,15 +1483,16 @@ private static Method findInterfaceMethodIfPossible(String methodName, Class<?>[
14831483
}
14841484

14851485
/**
1486-
* Get the closest publicly accessible (and exported) method in the supplied method's type
1487-
* hierarchy that has a method signature equivalent to the supplied method, if possible.
1488-
* <p>Otherwise, this method recursively searches the class hierarchy and implemented
1489-
* interfaces for an equivalent method that is {@code public} and declared in a
1490-
* {@code public} type.
1491-
* <p>If a publicly accessible equivalent method cannot be found, the supplied method
1492-
* will be returned, indicating that no such equivalent method exists. Consequently,
1493-
* callers of this method must manually validate the accessibility of the returned method
1494-
* if public access is a requirement.
1486+
* Get the closest publicly accessible method in the supplied method's type hierarchy that
1487+
* has a method signature equivalent to the supplied method, if possible.
1488+
* <p>This method recursively searches the class hierarchy and implemented interfaces for
1489+
* an equivalent method that is {@code public}, declared in a {@code public} type, and
1490+
* {@linkplain Module#isExported(String, Module) exported} to {@code spring-core}.
1491+
* <p>If the supplied method is not {@code public} or is {@code static}, or if a publicly
1492+
* accessible equivalent method cannot be found, the supplied method will be returned,
1493+
* indicating that no such equivalent method exists. Consequently, callers of this method
1494+
* must manually validate the accessibility of the returned method if public access is a
1495+
* requirement.
14951496
* <p>This is particularly useful for arriving at a public exported type on the Java
14961497
* Module System which allows the method to be invoked via reflection without an illegal
14971498
* access warning. This is also useful for invoking methods via a public API in bytecode
@@ -1508,10 +1509,11 @@ private static Method findInterfaceMethodIfPossible(String methodName, Class<?>[
15081509
*/
15091510
public static Method getPubliclyAccessibleMethodIfPossible(Method method, @Nullable Class<?> targetClass) {
15101511
Class<?> declaringClass = method.getDeclaringClass();
1511-
// If the method is not public or its declaring class is public and exported already,
1512-
// we can abort the search immediately (avoiding reflection as well as cache access).
1513-
if (!Modifier.isPublic(method.getModifiers()) || (Modifier.isPublic(declaringClass.getModifiers()) &&
1514-
declaringClass.getModule().isExported(declaringClass.getPackageName(), ClassUtils.class.getModule()))) {
1512+
// If the method is not public, or it's static, or its declaring class is public and exported
1513+
// already, we can abort the search immediately (avoiding reflection as well as cache access).
1514+
if (!Modifier.isPublic(method.getModifiers()) || Modifier.isStatic(method.getModifiers()) ||
1515+
(Modifier.isPublic(declaringClass.getModifiers()) &&
1516+
declaringClass.getModule().isExported(declaringClass.getPackageName(), ClassUtils.class.getModule()))) {
15151517
return method;
15161518
}
15171519

spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,49 @@ void privateSubclassOverridesMethodInPublicSuperclass() throws Exception {
868868
assertPubliclyAccessible(publiclyAccessibleMethod);
869869
}
870870

871+
@Test // gh-35667
872+
void staticMethodInPublicClass() throws Exception {
873+
Method originalMethod = PublicSuperclass.class.getMethod("getCacheKey");
874+
875+
// Prerequisite: method must be public static for this use case.
876+
assertPublic(originalMethod);
877+
assertStatic(originalMethod);
878+
879+
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
880+
assertThat(publiclyAccessibleMethod).isSameAs(originalMethod);
881+
assertPubliclyAccessible(publiclyAccessibleMethod);
882+
}
883+
884+
@Test // gh-35667
885+
void publicSubclassHidesStaticMethodInPublicSuperclass() throws Exception {
886+
Method originalMethod = PublicSubclass.class.getMethod("getCacheKey");
887+
888+
// Prerequisite: type must be public for this use case.
889+
assertPublic(originalMethod.getDeclaringClass());
890+
// Prerequisite: method must be public static for this use case.
891+
assertPublic(originalMethod);
892+
assertStatic(originalMethod);
893+
894+
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
895+
assertThat(publiclyAccessibleMethod).isSameAs(originalMethod);
896+
assertPubliclyAccessible(publiclyAccessibleMethod);
897+
}
898+
899+
@Test // gh-35667
900+
void privateSubclassHidesStaticMethodInPublicSuperclass() throws Exception {
901+
Method originalMethod = PrivateSubclass.class.getMethod("getCacheKey");
902+
903+
// Prerequisite: type must not be public for this use case.
904+
assertNotPublic(originalMethod.getDeclaringClass());
905+
// Prerequisite: method must be public static for this use case.
906+
assertPublic(originalMethod);
907+
assertStatic(originalMethod);
908+
909+
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
910+
assertThat(publiclyAccessibleMethod).isSameAs(originalMethod);
911+
assertNotPubliclyAccessible(publiclyAccessibleMethod);
912+
}
913+
871914
}
872915

873916

@@ -914,6 +957,10 @@ private static boolean isPublic(Member member) {
914957
return Modifier.isPublic(member.getModifiers());
915958
}
916959

960+
private static void assertStatic(Member member) {
961+
assertThat(Modifier.isStatic(member.getModifiers())).as("%s must be static", member).isTrue();
962+
}
963+
917964

918965
@Target(ElementType.METHOD)
919966
@Retention(RetentionPolicy.RUNTIME)
@@ -1048,8 +1095,27 @@ private interface PrivateInterface {
10481095
String greet(String name);
10491096
}
10501097

1098+
public static class PublicSubclass extends PublicSuperclass {
1099+
1100+
/**
1101+
* This method intentionally has the exact same signature as
1102+
* {@link PublicSuperclass#getCacheKey()}.
1103+
*/
1104+
public static String getCacheKey() {
1105+
return "child";
1106+
}
1107+
}
1108+
10511109
private static class PrivateSubclass extends PublicSuperclass implements PublicInterface, PrivateInterface {
10521110

1111+
/**
1112+
* This method intentionally has the exact same signature as
1113+
* {@link PublicSuperclass#getCacheKey()}.
1114+
*/
1115+
public static String getCacheKey() {
1116+
return "child";
1117+
}
1118+
10531119
@Override
10541120
public int getNumber() {
10551121
return 2;

spring-core/src/test/java/org/springframework/util/PublicSuperclass.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@
2121
*/
2222
public class PublicSuperclass {
2323

24+
/**
25+
* This method intentionally has the exact same signature as
26+
* {@link org.springframework.util.ClassUtilsTests.PublicSubclass#getCacheKey()} and
27+
* {@link org.springframework.util.ClassUtilsTests.PrivateSubclass#getCacheKey()}.
28+
*/
29+
public static String getCacheKey() {
30+
return "parent";
31+
}
32+
2433
public String getMessage() {
2534
return "goodbye";
2635
}

0 commit comments

Comments
 (0)