From e3da26ebbd40a34eef5156765da04353ad9ee010 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 1 Oct 2025 19:55:42 +0200 Subject: [PATCH 1/2] Clarify event parameter type for multiple mapped classes Closes gh-35506 --- .../context/event/EventListener.java | 7 +++---- .../AnnotationDrivenEventListenerTests.java | 19 +++++++++++++++++-- .../context/event/test/TestEvent.java | 8 +++++++- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/event/EventListener.java b/spring-context/src/main/java/org/springframework/context/event/EventListener.java index e08a2a81171..0fc2a4d06bc 100644 --- a/spring-context/src/main/java/org/springframework/context/event/EventListener.java +++ b/spring-context/src/main/java/org/springframework/context/event/EventListener.java @@ -101,10 +101,9 @@ public @interface EventListener { /** * The event classes that this listener handles. - *

If this attribute is specified with a single value, the - * annotated method may optionally accept a single parameter. - * However, if this attribute is specified with multiple values, - * the annotated method must not declare any parameters. + *

The annotated method may optionally accept a single parameter + * of the given event class, or of a common base class or interface + * for all given event classes. */ @AliasFor("value") Class[] classes() default {}; diff --git a/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java b/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java index e105cc60e8f..e5dee62c44c 100644 --- a/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java @@ -16,6 +16,7 @@ package org.springframework.context.event; +import java.io.Serializable; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -105,8 +106,9 @@ class AnnotationDrivenEventListenerTests { this.eventCollector.assertTotalEventsCount(1); this.eventCollector.clear(); - this.context.publishEvent(event); - this.eventCollector.assertEvent(listener, event); + TestEvent otherEvent = new TestEvent(this, Integer.valueOf(1)); + this.context.publishEvent(otherEvent); + this.eventCollector.assertEvent(listener, otherEvent); this.eventCollector.assertTotalEventsCount(1); context.getBean(ApplicationEventMulticaster.class).removeApplicationListeners(l -> @@ -742,6 +744,11 @@ class AnnotationDrivenEventListenerTests { public void handleString(String content) { collectEvent(content); } + + @EventListener({Boolean.class, Integer.class}) + public void handleBooleanOrInteger(Serializable content) { + collectEvent(content); + } } @@ -1009,6 +1016,8 @@ class AnnotationDrivenEventListenerTests { void handleString(String payload); + void handleBooleanOrInteger(Serializable content); + void handleTimestamp(Long timestamp); void handleRatio(Double ratio); @@ -1031,6 +1040,12 @@ class AnnotationDrivenEventListenerTests { super.handleString(payload); } + @EventListener({Boolean.class, Integer.class}) + @Override + public void handleBooleanOrInteger(Serializable content) { + super.handleBooleanOrInteger(content); + } + @ConditionalEvent("#root.event.timestamp > #p0") @Override public void handleTimestamp(Long timestamp) { diff --git a/spring-context/src/test/java/org/springframework/context/event/test/TestEvent.java b/spring-context/src/test/java/org/springframework/context/event/test/TestEvent.java index deceb7dbe14..01b22813eaa 100644 --- a/spring-context/src/test/java/org/springframework/context/event/test/TestEvent.java +++ b/spring-context/src/test/java/org/springframework/context/event/test/TestEvent.java @@ -18,11 +18,12 @@ package org.springframework.context.event.test; /** * @author Stephane Nicoll + * @author Juergen Hoeller */ @SuppressWarnings("serial") public class TestEvent extends IdentifiableApplicationEvent { - public final String msg; + public final Object msg; public TestEvent(Object source, String id, String msg) { super(source, id); @@ -34,6 +35,11 @@ public class TestEvent extends IdentifiableApplicationEvent { this.msg = msg; } + public TestEvent(Object source, Integer msg) { + super(source); + this.msg = msg; + } + public TestEvent(Object source) { this(source, "test"); } From a6f6ecfe6c2d2f20b23c3d725614bc36c9c0e8f4 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 1 Oct 2025 19:56:23 +0200 Subject: [PATCH 2/2] Revise getPubliclyAccessibleMethodIfPossible to rely on Module#isExported This avoids reflection and cache access for regular public and exported types. Closes gh-35556 --- .../org/springframework/util/ClassUtils.java | 19 +++++++++----- .../springframework/util/ClassUtilsTests.java | 26 ++++++++++++++----- 2 files changed, 31 insertions(+), 14 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 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); }