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
This commit is contained in:
Sam Brannen 2025-09-10 14:23:59 +02:00
parent 4745c7cf3c
commit 0e3e34bee0
2 changed files with 27 additions and 7 deletions

View File

@ -18,6 +18,7 @@ package org.springframework.core.annotation;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -38,6 +39,7 @@ import org.springframework.util.StringUtils;
* interface-declared parameter annotations from the concrete target method. * interface-declared parameter annotations from the concrete target method.
* *
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Sam Brannen
* @since 6.1 * @since 6.1
* @see #getMethodAnnotation(Class) * @see #getMethodAnnotation(Class)
* @see #getMethodParameters() * @see #getMethodParameters()
@ -181,7 +183,7 @@ public class AnnotatedMethod {
clazz = null; clazz = null;
} }
if (clazz != null) { if (clazz != null) {
for (Method candidate : clazz.getMethods()) { for (Method candidate : clazz.getDeclaredMethods()) {
if (isOverrideFor(candidate)) { if (isOverrideFor(candidate)) {
parameterAnnotations.add(candidate.getParameterAnnotations()); parameterAnnotations.add(candidate.getParameterAnnotations());
} }
@ -194,8 +196,9 @@ public class AnnotatedMethod {
} }
private boolean isOverrideFor(Method candidate) { private boolean isOverrideFor(Method candidate) {
if (!candidate.getName().equals(this.method.getName()) || if (Modifier.isPrivate(candidate.getModifiers()) ||
candidate.getParameterCount() != this.method.getParameterCount()) { !candidate.getName().equals(this.method.getName()) ||
(candidate.getParameterCount() != this.method.getParameterCount())) {
return false; return false;
} }
Class<?>[] paramTypes = this.method.getParameterTypes(); Class<?>[] paramTypes = this.method.getParameterTypes();

View File

@ -19,12 +19,15 @@ package org.springframework.core.annotation;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.core.MethodParameter; 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; import static org.assertj.core.api.Assertions.assertThat;
/** /**
@ -55,6 +58,12 @@ class AnnotatedMethodTests {
@Test @Test
void shouldFindAnnotationOnMethodParameterInGenericAbstractSuperclass() { 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); Method processTwo = getMethod("processTwo", String.class);
AnnotatedMethod annotatedMethod = new AnnotatedMethod(processTwo); AnnotatedMethod annotatedMethod = new AnnotatedMethod(processTwo);
@ -78,7 +87,14 @@ class AnnotatedMethodTests {
private static Method getMethod(String name, Class<?>...parameterTypes) { 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 @Handler
public abstract void processTwo(@Param C value); // Intentionally NOT public
abstract void processTwo(@Param C value);
} }
static class GenericInterfaceImpl extends GenericAbstractSuperclass<String> { static class GenericInterfaceImpl extends GenericAbstractSuperclass<String> {
@Override @Override
public void processTwo(String value) { void processTwo(String value) {
} }
} }