Skip to content

Commit

Permalink
Invoke init/destroy/SpEL methods via public types whenever possible
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sbrannen committed Aug 22, 2024
1 parent cac623b commit 47f88e1
Show file tree
Hide file tree
Showing 15 changed files with 519 additions and 171 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,9 @@ private void addInitDestroyHint(Class<?> beanUserClass, String methodName) {
Method method = ReflectionUtils.findMethod(methodDeclaringClass, methodName);
if (method != null) {
this.hints.reflection().registerMethod(method, ExecutableMode.INVOKE);
Method interfaceMethod = ClassUtils.getInterfaceMethodIfPossible(method, beanUserClass);
if (!interfaceMethod.equals(method)) {
this.hints.reflection().registerMethod(interfaceMethod, ExecutableMode.INVOKE);
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(method, beanUserClass);
if (!publiclyAccessibleMethod.equals(method)) {
this.hints.reflection().registerMethod(publiclyAccessibleMethod, ExecutableMode.INVOKE);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1899,7 +1899,7 @@ protected void invokeCustomInitMethod(String beanName, Object bean, RootBeanDefi
if (logger.isTraceEnabled()) {
logger.trace("Invoking init method '" + methodName + "' on bean with name '" + beanName + "'");
}
Method methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(initMethod, beanClass);
Method methodToInvoke = ClassUtils.getPubliclyAccessibleMethodIfPossible(initMethod, beanClass);

try {
ReflectionUtils.makeAccessible(methodToInvoke);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ else if (paramTypes.length == 1 && boolean.class != paramTypes[0]) {
beanName + "' has a non-boolean parameter - not supported as destroy method");
}
}
destroyMethod = ClassUtils.getInterfaceMethodIfPossible(destroyMethod, bean.getClass());
destroyMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(destroyMethod, bean.getClass());
destroyMethods.add(destroyMethod);
}
}
Expand Down Expand Up @@ -253,8 +253,8 @@ else if (this.destroyMethodNames != null) {
for (String destroyMethodName : this.destroyMethodNames) {
Method destroyMethod = determineDestroyMethod(destroyMethodName);
if (destroyMethod != null) {
invokeCustomDestroyMethod(
ClassUtils.getInterfaceMethodIfPossible(destroyMethod, this.bean.getClass()));
destroyMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(destroyMethod, this.bean.getClass());
invokeCustomDestroyMethod(destroyMethod);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ private void privateDestroy() {

}

interface Initializable {
public interface Initializable {

void initialize();
}
Expand All @@ -643,7 +643,7 @@ public void initialize() {
}
}

interface Disposable {
public interface Disposable {

void dispose();
}
Expand Down
106 changes: 93 additions & 13 deletions spring-core/src/main/java/org/springframework/util/ClassUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,17 @@ public abstract class ClassUtils {
private static final Set<Class<?>> javaLanguageInterfaces;

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

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


static {
primitiveWrapperTypeMap.put(Boolean.class, boolean.class);
Expand Down Expand Up @@ -1394,7 +1401,7 @@ public static Method getMostSpecificMethod(Method method, @Nullable Class<?> tar
* @param method the method to be invoked, potentially from an implementation class
* @return the corresponding interface method, or the original method if none found
* @since 5.1
* @deprecated in favor of {@link #getInterfaceMethodIfPossible(Method, Class)}
* @deprecated in favor of {@link #getPubliclyAccessibleMethodIfPossible(Method, Class)}
*/
@Deprecated
public static Method getInterfaceMethodIfPossible(Method method) {
Expand All @@ -1407,45 +1414,118 @@ public static Method getInterfaceMethodIfPossible(Method method) {
* Module System which allows the method to be invoked via reflection without an illegal
* access warning.
* @param method the method to be invoked, potentially from an implementation class
* @param targetClass the target class to check for declared interfaces
* @param targetClass the target class to invoke the method on, or {@code null} if unknown
* @return the corresponding interface method, or the original method if none found
* @since 5.3.16
* @see #getPubliclyAccessibleMethodIfPossible(Method, Class)
* @see #getMostSpecificMethod
* @deprecated in favor of {@link #getPubliclyAccessibleMethodIfPossible(Method, Class)}
*/
@Deprecated(since = "6.2")
public static Method getInterfaceMethodIfPossible(Method method, @Nullable Class<?> targetClass) {
if (!Modifier.isPublic(method.getModifiers()) || method.getDeclaringClass().isInterface()) {
Class<?> declaringClass = method.getDeclaringClass();
if (!Modifier.isPublic(method.getModifiers()) || declaringClass.isInterface()) {
return method;
}
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();

// Try cached version of method in its declaring class
Method result = interfaceMethodCache.computeIfAbsent(method,
key -> findInterfaceMethodIfPossible(key, key.getParameterTypes(), key.getDeclaringClass(),
Object.class));
if (result == method && targetClass != null) {
key -> findInterfaceMethodIfPossible(methodName, parameterTypes, declaringClass, Object.class));
if (result == null && targetClass != null) {
// No interface method found yet -> try given target class (possibly a subclass of the
// declaring class, late-binding a base class method to a subclass-declared interface:
// see e.g. HashMap.HashIterator.hasNext)
result = findInterfaceMethodIfPossible(method, method.getParameterTypes(), targetClass,
method.getDeclaringClass());
result = findInterfaceMethodIfPossible(methodName, parameterTypes, targetClass, declaringClass);
}
return result;
return (result != null ? result : method);
}

private static Method findInterfaceMethodIfPossible(Method method, Class<?>[] parameterTypes,
@Nullable
private static Method findInterfaceMethodIfPossible(String methodName, Class<?>[] parameterTypes,
Class<?> startClass, Class<?> endClass) {

Class<?> current = startClass;
while (current != null && current != endClass) {
for (Class<?> ifc : current.getInterfaces()) {
try {
return ifc.getMethod(method.getName(), parameterTypes);
if (Modifier.isPublic(ifc.getModifiers())) {
return ifc.getMethod(methodName, parameterTypes);
}
}
catch (NoSuchMethodException ex) {
// ignore
}
}
current = current.getSuperclass();
}
return method;
return null;
}

/**
* Get the first publicly accessible method in the supplied method's type hierarchy that
* has a method signature equivalent to the supplied method, if possible.
* <p>If the supplied method is {@code public} and declared in a {@code public} type,
* the supplied method will be returned.
* <p>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.
* <p>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.
* <p>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
* &mdash; for example, for use with the Spring Expression Language (SpEL) compiler.
* For example, if a non-public class overrides {@code toString()}, this method will
* traverse up the type hierarchy to find the first public type that declares the method
* (if there is one). For {@code toString()}, it may traverse as far as {@link Object}.
* @param method the method to be invoked, potentially from an implementation class
* @param targetClass the target class to invoke the method on, or {@code null} if unknown
* @return the corresponding publicly accessible method, or the original method if none
* found
* @since 6.2
* @see #getInterfaceMethodIfPossible(Method, Class)
* @see #getMostSpecificMethod(Method, Class)
*/
public static Method getPubliclyAccessibleMethodIfPossible(Method method, @Nullable Class<?> targetClass) {
Class<?> declaringClass = method.getDeclaringClass();
// If the method is not public, we can abort the search immediately; or if the method's
// declaring class is public, the method is already publicly accessible.
if (!Modifier.isPublic(method.getModifiers()) || Modifier.isPublic(declaringClass.getModifiers())) {
return method;
}

Method interfaceMethod = getInterfaceMethodIfPossible(method, targetClass);
// If we found a method in a public interface, return the interface method.
if (!interfaceMethod.equals(method)) {
return interfaceMethod;
}

Method result = publiclyAccessibleMethodCache.computeIfAbsent(method,
key -> findPubliclyAccessibleMethodIfPossible(key.getName(), key.getParameterTypes(), declaringClass));
return (result != null ? result : method);
}

@Nullable
private static Method findPubliclyAccessibleMethodIfPossible(
String methodName, Class<?>[] parameterTypes, Class<?> declaringClass) {

Class<?> current = declaringClass.getSuperclass();
while (current != null) {
if (Modifier.isPublic(current.getModifiers())) {
try {
return current.getDeclaredMethod(methodName, parameterTypes);
}
catch (NoSuchMethodException ex) {
// ignore
}
}
current = current.getSuperclass();
}
return null;
}

/**
Expand Down
Loading

0 comments on commit 47f88e1

Please sign in to comment.