From 0e3e34bee0c5b452b51c6ff6184e071a73ee2d40 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 10 Sep 2025 14:23:59 +0200 Subject: [PATCH] Find annotations on parameters in overridden non-public methods Prior to this commit, annotations were not found on parameters in an overridden method unless the method was public. Specifically, the search algorithm in AnnotatedMethod did not consider a protected or package-private method in a superclass to be a potential override candidate. This affects parameter annotation searches in spring-messaging, spring-webmvc, spring-webflux, and any other components that use or extend AnnotatedMethod. To address that, this commit revises the search algorithm in AnnotatedMethod to consider all non-final declared methods as potential override candidates, thereby aligning with the search logic in AnnotationsScanner for the MergedAnnotations API. Closes gh-35349 --- .../core/annotation/AnnotatedMethod.java | 9 ++++--- .../core/annotation/AnnotatedMethodTests.java | 25 ++++++++++++++++--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedMethod.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedMethod.java index ed9ed52530..519133a623 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedMethod.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedMethod.java @@ -18,6 +18,7 @@ package org.springframework.core.annotation; import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -38,6 +39,7 @@ import org.springframework.util.StringUtils; * interface-declared parameter annotations from the concrete target method. * * @author Juergen Hoeller + * @author Sam Brannen * @since 6.1 * @see #getMethodAnnotation(Class) * @see #getMethodParameters() @@ -181,7 +183,7 @@ public class AnnotatedMethod { clazz = null; } if (clazz != null) { - for (Method candidate : clazz.getMethods()) { + for (Method candidate : clazz.getDeclaredMethods()) { if (isOverrideFor(candidate)) { parameterAnnotations.add(candidate.getParameterAnnotations()); } @@ -194,8 +196,9 @@ public class AnnotatedMethod { } private boolean isOverrideFor(Method candidate) { - if (!candidate.getName().equals(this.method.getName()) || - candidate.getParameterCount() != this.method.getParameterCount()) { + if (Modifier.isPrivate(candidate.getModifiers()) || + !candidate.getName().equals(this.method.getName()) || + (candidate.getParameterCount() != this.method.getParameterCount())) { return false; } Class[] paramTypes = this.method.getParameterTypes(); diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedMethodTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedMethodTests.java index 1118239d61..c2c073e624 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedMethodTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedMethodTests.java @@ -19,12 +19,15 @@ package org.springframework.core.annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import org.junit.jupiter.api.Test; import org.springframework.core.MethodParameter; -import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; import static org.assertj.core.api.Assertions.assertThat; /** @@ -55,6 +58,12 @@ class AnnotatedMethodTests { @Test void shouldFindAnnotationOnMethodParameterInGenericAbstractSuperclass() { + // Prerequisites for gh-35349 + Method abstractMethod = ReflectionUtils.findMethod(GenericAbstractSuperclass.class, "processTwo", Object.class); + assertThat(abstractMethod).isNotNull(); + assertThat(Modifier.isAbstract(abstractMethod.getModifiers())).as("abstract").isTrue(); + assertThat(Modifier.isPublic(abstractMethod.getModifiers())).as("public").isFalse(); + Method processTwo = getMethod("processTwo", String.class); AnnotatedMethod annotatedMethod = new AnnotatedMethod(processTwo); @@ -78,7 +87,14 @@ class AnnotatedMethodTests { private static Method getMethod(String name, Class...parameterTypes) { - return ClassUtils.getMethod(GenericInterfaceImpl.class, name, parameterTypes); + Class clazz = GenericInterfaceImpl.class; + Method method = ReflectionUtils.findMethod(clazz, name, parameterTypes); + if (method == null) { + String parameterNames = stream(parameterTypes).map(Class::getName).collect(joining(", ")); + throw new IllegalStateException("Expected method not found: %s#%s(%s)" + .formatted(clazz.getSimpleName(), name, parameterNames)); + } + return method; } @@ -103,13 +119,14 @@ class AnnotatedMethodTests { } @Handler - public abstract void processTwo(@Param C value); + // Intentionally NOT public + abstract void processTwo(@Param C value); } static class GenericInterfaceImpl extends GenericAbstractSuperclass { @Override - public void processTwo(String value) { + void processTwo(String value) { } }