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 642fc54e107..6d33e434ce4 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -1483,8 +1483,8 @@ public abstract class ClassUtils { } /** - * Get the highest publicly accessible method in the supplied method's type hierarchy that - * has a method signature equivalent to the supplied method, if possible. + * 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. @@ -1507,18 +1507,21 @@ public abstract class ClassUtils { * @see #getMostSpecificMethod(Method, Class) */ public static Method getPubliclyAccessibleMethodIfPossible(Method method, @Nullable Class> targetClass) { - // If the method is not public, we can abort the search immediately. - if (!Modifier.isPublic(method.getModifiers())) { + 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()))) { return method; } Method interfaceMethod = getInterfaceMethodIfPossible(method, targetClass, true); // If we found a method in a public interface, return the interface method. - if (interfaceMethod != method) { + if (interfaceMethod != method && interfaceMethod.getDeclaringClass().getModule().isExported( + interfaceMethod.getDeclaringClass().getPackageName(), ClassUtils.class.getModule())) { return interfaceMethod; } - Class> declaringClass = method.getDeclaringClass(); // Bypass cache for java.lang.Object unless it is actually an overridable method declared there. if (declaringClass.getSuperclass() == Object.class && !ReflectionUtils.isObjectMethod(method)) { return method; @@ -1540,7 +1543,9 @@ public abstract class ClassUtils { if (method == null) { break; } - if (Modifier.isPublic(method.getDeclaringClass().getModifiers())) { + if (Modifier.isPublic(method.getDeclaringClass().getModifiers()) && + method.getDeclaringClass().getModule().isExported( + method.getDeclaringClass().getPackageName(), ClassUtils.class.getModule())) { result = method; } current = method.getDeclaringClass().getSuperclass(); 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 9ae83e3d91a..fbdd01dafa2 100644 --- a/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java @@ -27,6 +27,7 @@ import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; +import java.net.URLConnection; import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; @@ -687,13 +688,13 @@ class ClassUtilsTests { } @Test - void publicMethodInObjectClass() throws Exception { + void publicMethodInPublicClass() throws Exception { Class> originalType = String.class; - Method originalMethod = originalType.getDeclaredMethod("hashCode"); + Method originalMethod = originalType.getDeclaredMethod("toString"); Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null); - assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(Object.class); - assertThat(publiclyAccessibleMethod.getName()).isEqualTo("hashCode"); + assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(originalType); + assertThat(publiclyAccessibleMethod).isSameAs(originalMethod); assertPubliclyAccessible(publiclyAccessibleMethod); } @@ -703,9 +704,20 @@ class ClassUtilsTests { Method originalMethod = originalType.getDeclaredMethod("size"); Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null); - // Should find the interface method in List. - assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(List.class); - assertThat(publiclyAccessibleMethod.getName()).isEqualTo("size"); + // Should not find the interface method in List. + assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(originalType); + assertThat(publiclyAccessibleMethod).isSameAs(originalMethod); + assertPubliclyAccessible(publiclyAccessibleMethod); + } + + @Test + void publicMethodInNonExportedClass() throws Exception { + Class> originalType = getClass().getClassLoader().loadClass("sun.net.www.protocol.http.HttpURLConnection"); + Method originalMethod = originalType.getDeclaredMethod("getOutputStream"); + + Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null); + assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(URLConnection.class); + assertThat(publiclyAccessibleMethod.getName()).isSameAs(originalMethod.getName()); assertPubliclyAccessible(publiclyAccessibleMethod); }