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
This commit is contained in:
Sam Brannen
2024-07-14 12:01:25 +02:00
parent cac623b3f4
commit 47f88e123f
15 changed files with 519 additions and 171 deletions

View File

@@ -23,12 +23,17 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
@@ -37,6 +42,7 @@ import a.ClassHavingNestedClass;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
@@ -503,6 +509,278 @@ class ClassUtilsTests {
}
@Nested // gh-33216
class GetPubliclyAccessibleMethodTests {
@Test
void nonPublicMethod(TestInfo testInfo) {
Method originalMethod = testInfo.getTestMethod().get();
// Prerequisites for this use case:
assertNotPublic(originalMethod);
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
assertThat(publiclyAccessibleMethod).isSameAs(originalMethod);
assertNotPubliclyAccessible(publiclyAccessibleMethod);
}
@Test
// This method is intentionally public.
public void publicMethodInNonPublicType(TestInfo testInfo) {
Method originalMethod = testInfo.getTestMethod().get();
// Prerequisites for this use case:
assertPublic(originalMethod);
assertNotPublic(originalMethod.getDeclaringClass());
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
assertThat(publiclyAccessibleMethod).isSameAs(originalMethod);
assertNotPubliclyAccessible(publiclyAccessibleMethod);
}
@Test
void publicMethodInPublicType() throws Exception {
Class<?> originalType = String.class;
Method originalMethod = originalType.getDeclaredMethod("toString");
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(originalType);
assertThat(publiclyAccessibleMethod).isSameAs(originalMethod);
assertPubliclyAccessible(publiclyAccessibleMethod);
}
@Test
void publicInterfaceMethodInPublicType() throws Exception {
Class<?> originalType = ArrayList.class;
Method originalMethod = originalType.getDeclaredMethod("size");
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
// Should not find the interface method in List.
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(originalType);
assertThat(publiclyAccessibleMethod).isSameAs(originalMethod);
assertPubliclyAccessible(publiclyAccessibleMethod);
}
@Test
void publicMethodInJavaLangObjectDeclaredInNonPublicType() throws Exception {
List<String> unmodifiableList = Collections.unmodifiableList(Arrays.asList("foo", "bar"));
Class<?> targetClass = unmodifiableList.getClass();
// Prerequisites for this use case:
assertNotPublic(targetClass);
Method originalMethod = targetClass.getMethod("toString");
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(Object.class);
assertThat(publiclyAccessibleMethod.getName()).isEqualTo("toString");
assertThat(publiclyAccessibleMethod.getParameterTypes()).isEmpty();
assertPubliclyAccessible(publiclyAccessibleMethod);
}
@Test
void publicMethodInJavaTimeZoneIdDeclaredInNonPublicSubclass() throws Exception {
// Returns a package-private java.time.ZoneRegion.
ZoneId zoneId = ZoneId.of("CET");
Class<?> targetClass = zoneId.getClass();
// Prerequisites for this use case:
assertNotPublic(targetClass);
Method originalMethod = targetClass.getDeclaredMethod("getId");
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(ZoneId.class);
assertThat(publiclyAccessibleMethod.getName()).isEqualTo("getId");
assertThat(publiclyAccessibleMethod.getParameterTypes()).isEmpty();
assertPubliclyAccessible(publiclyAccessibleMethod);
}
@Test
void publicInterfaceMethodDeclaredInNonPublicTypeWithLateBindingOfClassMethodToSubclassDeclaredInterface() throws Exception {
HashMap<String, String> hashMap = new HashMap<>();
// Returns a package-private java.util.HashMap.KeyIterator which extends java.util.HashMap.HashIterator
// which declares hasNext(), even though HashIterator does not implement Iterator. Rather, KeyIterator
// implements HashIterator.
Iterator<String> iterator = hashMap.keySet().iterator();
Class<?> targetClass = iterator.getClass();
// Prerequisites for this use case:
assertNotPublic(targetClass);
Method originalMethod = targetClass.getMethod("hasNext");
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, targetClass);
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(Iterator.class);
assertThat(publiclyAccessibleMethod.getName()).isEqualTo("hasNext");
assertThat(publiclyAccessibleMethod.getParameterTypes()).isEmpty();
assertPubliclyAccessible(publiclyAccessibleMethod);
}
@Test
void privateSubclassOverridesPropertyInPublicInterface() throws Exception {
Method originalMethod = PrivateSubclass.class.getDeclaredMethod("getText");
// Prerequisite: type must not be public for this use case.
assertNotPublic(originalMethod.getDeclaringClass());
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(PublicInterface.class);
assertThat(publiclyAccessibleMethod.getName()).isEqualTo("getText");
assertThat(publiclyAccessibleMethod.getParameterTypes()).isEmpty();
assertPubliclyAccessible(publiclyAccessibleMethod);
}
@Test
void privateSubclassOverridesPropertyInPrivateInterface() throws Exception {
Method originalMethod = PrivateSubclass.class.getDeclaredMethod("getMessage");
// Prerequisite: type must not be public for this use case.
assertNotPublic(originalMethod.getDeclaringClass());
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
// Should not find the interface method in PrivateInterface.
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(PublicSuperclass.class);
assertThat(publiclyAccessibleMethod.getName()).isEqualTo("getMessage");
assertThat(publiclyAccessibleMethod.getParameterTypes()).isEmpty();
assertPubliclyAccessible(publiclyAccessibleMethod);
}
@Test
void privateSubclassOverridesPropertyInPublicSuperclass() throws Exception {
Method originalMethod = PrivateSubclass.class.getDeclaredMethod("getNumber");
// Prerequisite: type must not be public for this use case.
assertNotPublic(originalMethod.getDeclaringClass());
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(PublicSuperclass.class);
assertThat(publiclyAccessibleMethod.getName()).isEqualTo("getNumber");
assertThat(publiclyAccessibleMethod.getParameterTypes()).isEmpty();
assertPubliclyAccessible(publiclyAccessibleMethod);
}
@Test
void packagePrivateSubclassOverridesMethodInPublicInterface() throws Exception {
List<String> unmodifiableList = Collections.unmodifiableList(Arrays.asList("foo", "bar"));
Class<?> targetClass = unmodifiableList.getClass();
// Prerequisites for this use case:
assertNotPublic(targetClass);
Method originalMethod = targetClass.getMethod("contains", Object.class);
// Prerequisite: type must not be public for this use case.
assertNotPublic(originalMethod.getDeclaringClass());
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(Collection.class);
assertThat(publiclyAccessibleMethod.getName()).isEqualTo("contains");
assertThat(publiclyAccessibleMethod.getParameterTypes()).containsExactly(Object.class);
assertPubliclyAccessible(publiclyAccessibleMethod);
}
@Test
void privateSubclassOverridesMethodInPrivateInterface() throws Exception {
Method originalMethod = PrivateSubclass.class.getMethod("greet", String.class);
// Prerequisite: type must not be public for this use case.
assertNotPublic(originalMethod.getDeclaringClass());
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(PublicSuperclass.class);
assertThat(publiclyAccessibleMethod.getName()).isEqualTo("greet");
assertThat(publiclyAccessibleMethod.getParameterTypes()).containsExactly(String.class);
assertPubliclyAccessible(publiclyAccessibleMethod);
}
@Test
void privateSubclassOverridesMethodInPublicSuperclass() throws Exception {
Method originalMethod = PrivateSubclass.class.getMethod("process", int.class);
// Prerequisite: type must not be public for this use case.
assertNotPublic(originalMethod.getDeclaringClass());
Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(originalMethod, null);
assertThat(publiclyAccessibleMethod.getDeclaringClass()).isEqualTo(PublicSuperclass.class);
assertThat(publiclyAccessibleMethod.getName()).isEqualTo("process");
assertThat(publiclyAccessibleMethod.getParameterTypes()).containsExactly(int.class);
assertPubliclyAccessible(publiclyAccessibleMethod);
}
private static void assertPubliclyAccessible(Method method) {
assertPublic(method);
assertPublic(method.getDeclaringClass());
}
private static void assertNotPubliclyAccessible(Method method) {
assertThat(!isPublic(method) || !isPublic(method.getDeclaringClass()))
.as("%s must not be publicly accessible", method)
.isTrue();
}
private static void assertPublic(Member member) {
assertThat(isPublic(member)).as("%s must be public", member).isTrue();
}
private static void assertPublic(Class<?> clazz) {
assertThat(isPublic(clazz)).as("%s must be public", clazz).isTrue();
}
private static void assertNotPublic(Member member) {
assertThat(!isPublic(member)).as("%s must be not be public", member).isTrue();
}
private static void assertNotPublic(Class<?> clazz) {
assertThat(!isPublic(clazz)).as("%s must be not be public", clazz).isTrue();
}
private static boolean isPublic(Class<?> clazz) {
return Modifier.isPublic(clazz.getModifiers());
}
private static boolean isPublic(Member member) {
return Modifier.isPublic(member.getModifiers());
}
private interface PrivateInterface {
String getMessage();
String greet(String name);
}
private static class PrivateSubclass extends PublicSuperclass implements PublicInterface, PrivateInterface {
@Override
public int getNumber() {
return 2;
}
@Override
public String getMessage() {
return "hello";
}
@Override
public String greet(String name) {
return "Hello, " + name;
}
@Override
public int process(int num) {
return num * 2;
}
@Override
public String getText() {
return "enigma";
}
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ValueSource(classes = { Boolean.class, Character.class, Byte.class, Short.class,

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.util;
/**
* This is intentionally a top-level public interface.
*/
public interface PublicInterface {
String getText();
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.util;
/**
* This is intentionally a top-level public class.
*/
public class PublicSuperclass {
public String getMessage() {
return "goodbye";
}
public int getNumber() {
return 1;
}
public String greet(String name) {
return "Super, " + name;
}
public int process(int num) {
return num + 1;
}
}