From b1f5b61bcd79fca7e697ec9ea263125cd7e7b557 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 26 Oct 2025 15:08:03 +0100 Subject: [PATCH] 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 --- .../org/springframework/util/ClassUtils.java | 28 ++++---- .../springframework/util/ClassUtilsTests.java | 66 +++++++++++++++++++ .../util/PublicSuperclass.java | 9 +++ 3 files changed, 90 insertions(+), 13 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java index 04bf8a42f67..15df17074de 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -1483,15 +1483,16 @@ public abstract class ClassUtils { } /** - * Get the closest publicly accessible (and exported) method in the supplied method's type - * hierarchy that has a method signature equivalent to the supplied method, if possible. - *

Otherwise, this method recursively searches the class hierarchy and implemented - * interfaces for an equivalent method that is {@code public} and declared in a - * {@code public} type. - *

If a publicly accessible equivalent method cannot be found, the supplied method - * will be returned, indicating that no such equivalent method exists. Consequently, - * callers of this method must manually validate the accessibility of the returned method - * if public access is a requirement. + * Get the closest publicly accessible method in the supplied method's type hierarchy that + * has a method signature equivalent to the supplied method, if possible. + *

This method recursively searches the class hierarchy and implemented interfaces for + * an equivalent method that is {@code public}, declared in a {@code public} type, and + * {@linkplain Module#isExported(String, Module) exported} to {@code spring-core}. + *

If the supplied method is not {@code public} or is {@code static}, or if a publicly + * accessible equivalent method cannot be found, the supplied method will be returned, + * indicating that no such equivalent method exists. Consequently, callers of this method + * must manually validate the accessibility of the returned method if public access is a + * requirement. *

This is particularly useful for arriving at a public exported type on the Java * Module System which allows the method to be invoked via reflection without an illegal * access warning. This is also useful for invoking methods via a public API in bytecode @@ -1508,10 +1509,11 @@ public abstract class ClassUtils { */ public static Method getPubliclyAccessibleMethodIfPossible(Method method, @Nullable Class targetClass) { Class declaringClass = method.getDeclaringClass(); - // If the method is not public or its declaring class is public and exported already, - // we can abort the search immediately (avoiding reflection as well as cache access). - if (!Modifier.isPublic(method.getModifiers()) || (Modifier.isPublic(declaringClass.getModifiers()) && - declaringClass.getModule().isExported(declaringClass.getPackageName(), ClassUtils.class.getModule()))) { + // If the method is not public, or it's static, or its declaring class is public and exported + // already, we can abort the search immediately (avoiding reflection as well as cache access). + if (!Modifier.isPublic(method.getModifiers()) || Modifier.isStatic(method.getModifiers()) || + (Modifier.isPublic(declaringClass.getModifiers()) && + declaringClass.getModule().isExported(declaringClass.getPackageName(), ClassUtils.class.getModule()))) { return method; } diff --git a/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java b/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java index fbdd01dafa2..20ea07a57a1 100644 --- a/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java @@ -868,6 +868,49 @@ class ClassUtilsTests { assertPubliclyAccessible(publiclyAccessibleMethod); } + @Test // gh-35667 + void staticMethodInPublicClass() throws Exception { + Method originalMethod = PublicSuperclass.class.getMethod("getCacheKey"); + + // Prerequisite: method must be public static for this use case. + assertPublic(originalMethod); + assertStatic(originalMethod); + + Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null); + assertThat(publiclyAccessibleMethod).isSameAs(originalMethod); + assertPubliclyAccessible(publiclyAccessibleMethod); + } + + @Test // gh-35667 + void publicSubclassHidesStaticMethodInPublicSuperclass() throws Exception { + Method originalMethod = PublicSubclass.class.getMethod("getCacheKey"); + + // Prerequisite: type must be public for this use case. + assertPublic(originalMethod.getDeclaringClass()); + // Prerequisite: method must be public static for this use case. + assertPublic(originalMethod); + assertStatic(originalMethod); + + Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null); + assertThat(publiclyAccessibleMethod).isSameAs(originalMethod); + assertPubliclyAccessible(publiclyAccessibleMethod); + } + + @Test // gh-35667 + void privateSubclassHidesStaticMethodInPublicSuperclass() throws Exception { + Method originalMethod = PrivateSubclass.class.getMethod("getCacheKey"); + + // Prerequisite: type must not be public for this use case. + assertNotPublic(originalMethod.getDeclaringClass()); + // Prerequisite: method must be public static for this use case. + assertPublic(originalMethod); + assertStatic(originalMethod); + + Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null); + assertThat(publiclyAccessibleMethod).isSameAs(originalMethod); + assertNotPubliclyAccessible(publiclyAccessibleMethod); + } + } @@ -914,6 +957,10 @@ class ClassUtilsTests { return Modifier.isPublic(member.getModifiers()); } + private static void assertStatic(Member member) { + assertThat(Modifier.isStatic(member.getModifiers())).as("%s must be static", member).isTrue(); + } + @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @@ -1048,8 +1095,27 @@ class ClassUtilsTests { String greet(String name); } + public static class PublicSubclass extends PublicSuperclass { + + /** + * This method intentionally has the exact same signature as + * {@link PublicSuperclass#getCacheKey()}. + */ + public static String getCacheKey() { + return "child"; + } + } + private static class PrivateSubclass extends PublicSuperclass implements PublicInterface, PrivateInterface { + /** + * This method intentionally has the exact same signature as + * {@link PublicSuperclass#getCacheKey()}. + */ + public static String getCacheKey() { + return "child"; + } + @Override public int getNumber() { return 2; diff --git a/spring-core/src/test/java/org/springframework/util/PublicSuperclass.java b/spring-core/src/test/java/org/springframework/util/PublicSuperclass.java index 0765a240458..da7c0a789b7 100644 --- a/spring-core/src/test/java/org/springframework/util/PublicSuperclass.java +++ b/spring-core/src/test/java/org/springframework/util/PublicSuperclass.java @@ -21,6 +21,15 @@ package org.springframework.util; */ public class PublicSuperclass { + /** + * This method intentionally has the exact same signature as + * {@link org.springframework.util.ClassUtilsTests.PublicSubclass#getCacheKey()} and + * {@link org.springframework.util.ClassUtilsTests.PrivateSubclass#getCacheKey()}. + */ + public static String getCacheKey() { + return "parent"; + } + public String getMessage() { return "goodbye"; }