Skip to content

Commit 47f88e1

Browse files
committed
Invoke init/destroy/SpEL methods via public types whenever possible
Prior to this commit, when invoking init methods and destroy methods for beans as well as methods within Spring Expression Language (SpEL) expressions via reflection, we invoked them based on the "interface method" returned from ClassUtils.getInterfaceMethodIfPossible(). That works well for finding methods defined in an interface, but it does not find public methods defined in a public superclass. For example, in a SpEL expression it was previously impossible to invoke toString() on a non-public type from a different module. This could be seen when attempting to invoke toString() on an unmodifiable list created by Collections.unmodifiableList(...). Doing so resulted in an InaccessibleObjectException. Although users can address that by adding an appropriate --add-opens declaration, such as --add-opens java.base/java.util=ALL-UNNAMED, it is better if applications do not have to add an --add-opens declaration for such use cases in SpEL. The same applies to init methods and destroy methods for beans. This commit therefore introduces a new getPubliclyAccessibleMethodIfPossible() method in ClassUtils which serves as a replacement for getInterfaceMethodIfPossible(). This new method finds the first publicly accessible method in the supplied method's type hierarchy that has a method signature equivalent to the supplied method. If the supplied method is public and declared in a public type, the supplied method will be returned. Otherwise, this method recursively searches the class hierarchy and implemented interfaces for an equivalent method that is public and declared in a public type. If a publicly accessible equivalent method cannot be found, the supplied method will be returned, indicating that no such equivalent method exists. All usage of getInterfaceMethodIfPossible() has been replaced with getPubliclyAccessibleMethodIfPossible() in spring-beans and spring-expression. In addition, getInterfaceMethodIfPossible() has been marked as deprecated in favor of the new method. As a bonus, the introduction of getPubliclyAccessibleMethodIfPossible() allows us to delete a fair amount of obsolete code within the SpEL infrastructure. See gh-29857 Closes gh-33216
1 parent cac623b commit 47f88e1

File tree

15 files changed

+519
-171
lines changed

15 files changed

+519
-171
lines changed

spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGenerator.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,9 @@ private void addInitDestroyHint(Class<?> beanUserClass, String methodName) {
176176
Method method = ReflectionUtils.findMethod(methodDeclaringClass, methodName);
177177
if (method != null) {
178178
this.hints.reflection().registerMethod(method, ExecutableMode.INVOKE);
179-
Method interfaceMethod = ClassUtils.getInterfaceMethodIfPossible(method, beanUserClass);
180-
if (!interfaceMethod.equals(method)) {
181-
this.hints.reflection().registerMethod(interfaceMethod, ExecutableMode.INVOKE);
179+
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(method, beanUserClass);
180+
if (!publiclyAccessibleMethod.equals(method)) {
181+
this.hints.reflection().registerMethod(publiclyAccessibleMethod, ExecutableMode.INVOKE);
182182
}
183183
}
184184
}

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1899,7 +1899,7 @@ protected void invokeCustomInitMethod(String beanName, Object bean, RootBeanDefi
18991899
if (logger.isTraceEnabled()) {
19001900
logger.trace("Invoking init method '" + methodName + "' on bean with name '" + beanName + "'");
19011901
}
1902-
Method methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(initMethod, beanClass);
1902+
Method methodToInvoke = ClassUtils.getPubliclyAccessibleMethodIfPossible(initMethod, beanClass);
19031903

19041904
try {
19051905
ReflectionUtils.makeAccessible(methodToInvoke);

spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ else if (paramTypes.length == 1 && boolean.class != paramTypes[0]) {
147147
beanName + "' has a non-boolean parameter - not supported as destroy method");
148148
}
149149
}
150-
destroyMethod = ClassUtils.getInterfaceMethodIfPossible(destroyMethod, bean.getClass());
150+
destroyMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(destroyMethod, bean.getClass());
151151
destroyMethods.add(destroyMethod);
152152
}
153153
}
@@ -253,8 +253,8 @@ else if (this.destroyMethodNames != null) {
253253
for (String destroyMethodName : this.destroyMethodNames) {
254254
Method destroyMethod = determineDestroyMethod(destroyMethodName);
255255
if (destroyMethod != null) {
256-
invokeCustomDestroyMethod(
257-
ClassUtils.getInterfaceMethodIfPossible(destroyMethod, this.bean.getClass()));
256+
destroyMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(destroyMethod, this.bean.getClass());
257+
invokeCustomDestroyMethod(destroyMethod);
258258
}
259259
}
260260
}

spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,7 @@ private void privateDestroy() {
631631

632632
}
633633

634-
interface Initializable {
634+
public interface Initializable {
635635

636636
void initialize();
637637
}
@@ -643,7 +643,7 @@ public void initialize() {
643643
}
644644
}
645645

646-
interface Disposable {
646+
public interface Disposable {
647647

648648
void dispose();
649649
}

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

+93-13
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,17 @@ public abstract class ClassUtils {
135135
private static final Set<Class<?>> javaLanguageInterfaces;
136136

137137
/**
138-
* Cache for equivalent methods on an interface implemented by the declaring class.
138+
* Cache for equivalent methods on a public interface implemented by the declaring class.
139139
*/
140140
private static final Map<Method, Method> interfaceMethodCache = new ConcurrentReferenceHashMap<>(256);
141141

142+
/**
143+
* Cache for equivalent public methods in a public declaring type within the type hierarchy
144+
* of the method's declaring class.
145+
* @since 6.2
146+
*/
147+
private static final Map<Method, Method> publiclyAccessibleMethodCache = new ConcurrentReferenceHashMap<>(256);
148+
142149

143150
static {
144151
primitiveWrapperTypeMap.put(Boolean.class, boolean.class);
@@ -1394,7 +1401,7 @@ public static Method getMostSpecificMethod(Method method, @Nullable Class<?> tar
13941401
* @param method the method to be invoked, potentially from an implementation class
13951402
* @return the corresponding interface method, or the original method if none found
13961403
* @since 5.1
1397-
* @deprecated in favor of {@link #getInterfaceMethodIfPossible(Method, Class)}
1404+
* @deprecated in favor of {@link #getPubliclyAccessibleMethodIfPossible(Method, Class)}
13981405
*/
13991406
@Deprecated
14001407
public static Method getInterfaceMethodIfPossible(Method method) {
@@ -1407,45 +1414,118 @@ public static Method getInterfaceMethodIfPossible(Method method) {
14071414
* Module System which allows the method to be invoked via reflection without an illegal
14081415
* access warning.
14091416
* @param method the method to be invoked, potentially from an implementation class
1410-
* @param targetClass the target class to check for declared interfaces
1417+
* @param targetClass the target class to invoke the method on, or {@code null} if unknown
14111418
* @return the corresponding interface method, or the original method if none found
14121419
* @since 5.3.16
1420+
* @see #getPubliclyAccessibleMethodIfPossible(Method, Class)
14131421
* @see #getMostSpecificMethod
1422+
* @deprecated in favor of {@link #getPubliclyAccessibleMethodIfPossible(Method, Class)}
14141423
*/
1424+
@Deprecated(since = "6.2")
14151425
public static Method getInterfaceMethodIfPossible(Method method, @Nullable Class<?> targetClass) {
1416-
if (!Modifier.isPublic(method.getModifiers()) || method.getDeclaringClass().isInterface()) {
1426+
Class<?> declaringClass = method.getDeclaringClass();
1427+
if (!Modifier.isPublic(method.getModifiers()) || declaringClass.isInterface()) {
14171428
return method;
14181429
}
1430+
String methodName = method.getName();
1431+
Class<?>[] parameterTypes = method.getParameterTypes();
1432+
14191433
// Try cached version of method in its declaring class
14201434
Method result = interfaceMethodCache.computeIfAbsent(method,
1421-
key -> findInterfaceMethodIfPossible(key, key.getParameterTypes(), key.getDeclaringClass(),
1422-
Object.class));
1423-
if (result == method && targetClass != null) {
1435+
key -> findInterfaceMethodIfPossible(methodName, parameterTypes, declaringClass, Object.class));
1436+
if (result == null && targetClass != null) {
14241437
// No interface method found yet -> try given target class (possibly a subclass of the
14251438
// declaring class, late-binding a base class method to a subclass-declared interface:
14261439
// see e.g. HashMap.HashIterator.hasNext)
1427-
result = findInterfaceMethodIfPossible(method, method.getParameterTypes(), targetClass,
1428-
method.getDeclaringClass());
1440+
result = findInterfaceMethodIfPossible(methodName, parameterTypes, targetClass, declaringClass);
14291441
}
1430-
return result;
1442+
return (result != null ? result : method);
14311443
}
14321444

1433-
private static Method findInterfaceMethodIfPossible(Method method, Class<?>[] parameterTypes,
1445+
@Nullable
1446+
private static Method findInterfaceMethodIfPossible(String methodName, Class<?>[] parameterTypes,
14341447
Class<?> startClass, Class<?> endClass) {
14351448

14361449
Class<?> current = startClass;
14371450
while (current != null && current != endClass) {
14381451
for (Class<?> ifc : current.getInterfaces()) {
14391452
try {
1440-
return ifc.getMethod(method.getName(), parameterTypes);
1453+
if (Modifier.isPublic(ifc.getModifiers())) {
1454+
return ifc.getMethod(methodName, parameterTypes);
1455+
}
14411456
}
14421457
catch (NoSuchMethodException ex) {
14431458
// ignore
14441459
}
14451460
}
14461461
current = current.getSuperclass();
14471462
}
1448-
return method;
1463+
return null;
1464+
}
1465+
1466+
/**
1467+
* Get the first publicly accessible method in the supplied method's type hierarchy that
1468+
* has a method signature equivalent to the supplied method, if possible.
1469+
* <p>If the supplied method is {@code public} and declared in a {@code public} type,
1470+
* the supplied method will be returned.
1471+
* <p>Otherwise, this method recursively searches the class hierarchy and implemented
1472+
* interfaces for an equivalent method that is {@code public} and declared in a
1473+
* {@code public} type.
1474+
* <p>If a publicly accessible equivalent method cannot be found, the supplied method
1475+
* will be returned, indicating that no such equivalent method exists. Consequently,
1476+
* callers of this method must manually validate the accessibility of the returned method
1477+
* if public access is a requirement.
1478+
* <p>This is particularly useful for arriving at a public exported type on the Java
1479+
* Module System which allows the method to be invoked via reflection without an illegal
1480+
* access warning. This is also useful for invoking methods via a public API in bytecode
1481+
* &mdash; for example, for use with the Spring Expression Language (SpEL) compiler.
1482+
* For example, if a non-public class overrides {@code toString()}, this method will
1483+
* traverse up the type hierarchy to find the first public type that declares the method
1484+
* (if there is one). For {@code toString()}, it may traverse as far as {@link Object}.
1485+
* @param method the method to be invoked, potentially from an implementation class
1486+
* @param targetClass the target class to invoke the method on, or {@code null} if unknown
1487+
* @return the corresponding publicly accessible method, or the original method if none
1488+
* found
1489+
* @since 6.2
1490+
* @see #getInterfaceMethodIfPossible(Method, Class)
1491+
* @see #getMostSpecificMethod(Method, Class)
1492+
*/
1493+
public static Method getPubliclyAccessibleMethodIfPossible(Method method, @Nullable Class<?> targetClass) {
1494+
Class<?> declaringClass = method.getDeclaringClass();
1495+
// If the method is not public, we can abort the search immediately; or if the method's
1496+
// declaring class is public, the method is already publicly accessible.
1497+
if (!Modifier.isPublic(method.getModifiers()) || Modifier.isPublic(declaringClass.getModifiers())) {
1498+
return method;
1499+
}
1500+
1501+
Method interfaceMethod = getInterfaceMethodIfPossible(method, targetClass);
1502+
// If we found a method in a public interface, return the interface method.
1503+
if (!interfaceMethod.equals(method)) {
1504+
return interfaceMethod;
1505+
}
1506+
1507+
Method result = publiclyAccessibleMethodCache.computeIfAbsent(method,
1508+
key -> findPubliclyAccessibleMethodIfPossible(key.getName(), key.getParameterTypes(), declaringClass));
1509+
return (result != null ? result : method);
1510+
}
1511+
1512+
@Nullable
1513+
private static Method findPubliclyAccessibleMethodIfPossible(
1514+
String methodName, Class<?>[] parameterTypes, Class<?> declaringClass) {
1515+
1516+
Class<?> current = declaringClass.getSuperclass();
1517+
while (current != null) {
1518+
if (Modifier.isPublic(current.getModifiers())) {
1519+
try {
1520+
return current.getDeclaredMethod(methodName, parameterTypes);
1521+
}
1522+
catch (NoSuchMethodException ex) {
1523+
// ignore
1524+
}
1525+
}
1526+
current = current.getSuperclass();
1527+
}
1528+
return null;
14491529
}
14501530

14511531
/**

0 commit comments

Comments
 (0)