mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Find annotation on overridden method in type hierarchy with unresolved generics
Prior to this commit, the MergedAnnotations support (specifically
AnnotationsScanner) and AnnotatedMethod did not find annotations on
overridden methods in type hierarchies with unresolved generics.
The reason for this is that ResolvableType.resolve() returns null for
such an unresolved type, which prevents the search algorithms from
considering such methods as override candidates.
For example, given the following type hierarchy, the compiler does not
generate a method corresponding to processOneAndTwo(Long, String) for
GenericInterfaceImpl. Nonetheless, one would expect an invocation of
processOneAndTwo(Long, String) to be @Transactional since it is
effectively an invocation of processOneAndTwo(Long, C) in
GenericAbstractSuperclass, which overrides/implements
processOneAndTwo(A, B) in GenericInterface, which is annotated with
@Transactional.
However, the MergedAnnotations infrastructure currently does not
determine that processOneAndTwo(Long, C) is @Transactional since it is
not able to determine that processOneAndTwo(Long, C) overrides
processOneAndTwo(A, B) because of the unresolved generic C.
interface GenericInterface<A, B> {
@Transactional
void processOneAndTwo(A value1, B value2);
}
abstract class GenericAbstractSuperclass<C> implements GenericInterface<Long, C> {
@Override
public void processOneAndTwo(Long value1, C value2) {
}
}
static GenericInterfaceImpl extends GenericAbstractSuperclass<String> {
}
To address such issues, this commit changes the logic in
AnnotationsScanner.hasSameGenericTypeParameters() and
AnnotatedMethod.isOverrideFor() so that they use
ResolvableType.toClass() instead of ResolvableType.resolve(). The
former returns Object.class for an unresolved generic which in turn
allows the search algorithms to properly detect method overrides in
such type hierarchies.
Closes gh-35342
This commit is contained in:
@@ -35,32 +35,46 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* Tests for {@link HandlerMethod}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
class HandlerMethodTests {
|
||||
|
||||
@Test
|
||||
void shouldValidateArgsWithConstraintsDirectlyOnClass() {
|
||||
void shouldValidateArgsWithConstraintsDirectlyInClass() {
|
||||
Object target = new MyClass();
|
||||
testValidateArgs(target, List.of("addIntValue", "addPersonAndIntValue", "addPersons", "addPeople", "addNames"), true);
|
||||
testValidateArgs(target, List.of("addPerson", "getPerson", "getIntValue", "addPersonNotValidated"), false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldValidateArgsWithConstraintsOnInterface() {
|
||||
void shouldValidateArgsWithConstraintsInInterface() {
|
||||
Object target = new MyInterfaceImpl();
|
||||
testValidateArgs(target, List.of("addIntValue", "addPersonAndIntValue", "addPersons", "addPeople"), true);
|
||||
testValidateArgs(target, List.of("addPerson", "addPersonNotValidated", "getPerson", "getIntValue"), false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldValidateReturnValueWithConstraintsDirectlyOnClass() {
|
||||
void shouldValidateArgsWithConstraintsInGenericAbstractSuperclass() {
|
||||
Object target = new GenericInterfaceImpl();
|
||||
shouldValidateArguments(getHandlerMethod(target, "processTwo", String.class), true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldValidateArgsWithConstraintsInGenericInterface() {
|
||||
Object target = new GenericInterfaceImpl();
|
||||
shouldValidateArguments(getHandlerMethod(target, "processOne", Long.class), false);
|
||||
shouldValidateArguments(getHandlerMethod(target, "processOneAndTwo", Long.class, Object.class), true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldValidateReturnValueWithConstraintsDirectlyInClass() {
|
||||
Object target = new MyClass();
|
||||
testValidateReturnValue(target, List.of("getPerson", "getIntValue"), true);
|
||||
testValidateReturnValue(target, List.of("addPerson", "addIntValue", "addPersonNotValidated"), false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldValidateReturnValueWithConstraintsOnInterface() {
|
||||
void shouldValidateReturnValueWithConstraintsInInterface() {
|
||||
Object target = new MyInterfaceImpl();
|
||||
testValidateReturnValue(target, List.of("getPerson", "getIntValue"), true);
|
||||
testValidateReturnValue(target, List.of("addPerson", "addIntValue", "addPersonNotValidated"), false);
|
||||
@@ -97,9 +111,19 @@ class HandlerMethodTests {
|
||||
assertThat(hm3.getResolvedFromHandlerMethod()).isSameAs(hm1);
|
||||
}
|
||||
|
||||
|
||||
private static void shouldValidateArguments(HandlerMethod handlerMethod, boolean expected) {
|
||||
if (expected) {
|
||||
assertThat(handlerMethod.shouldValidateArguments()).as(handlerMethod.getMethod().getName()).isTrue();
|
||||
}
|
||||
else {
|
||||
assertThat(handlerMethod.shouldValidateArguments()).as(handlerMethod.getMethod().getName()).isFalse();
|
||||
}
|
||||
}
|
||||
|
||||
private static void testValidateArgs(Object target, List<String> methodNames, boolean expected) {
|
||||
for (String methodName : methodNames) {
|
||||
assertThat(getHandlerMethod(target, methodName).shouldValidateArguments()).isEqualTo(expected);
|
||||
shouldValidateArguments(getHandlerMethod(target, methodName), expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +134,11 @@ class HandlerMethodTests {
|
||||
}
|
||||
|
||||
private static HandlerMethod getHandlerMethod(Object target, String methodName) {
|
||||
Method method = ClassUtils.getMethod(target.getClass(), methodName, (Class<?>[]) null);
|
||||
return getHandlerMethod(target, methodName, (Class<?>[]) null);
|
||||
}
|
||||
|
||||
private static HandlerMethod getHandlerMethod(Object target, String methodName, Class<?>... parameterTypes) {
|
||||
Method method = ClassUtils.getMethod(target.getClass(), methodName, parameterTypes);
|
||||
return new HandlerMethod(target, method).createWithValidateFlags();
|
||||
}
|
||||
|
||||
@@ -236,4 +264,32 @@ class HandlerMethodTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface GenericInterface<A, B> {
|
||||
|
||||
void processOne(@Valid A value1);
|
||||
|
||||
void processOneAndTwo(A value1, @Max(42) B value2);
|
||||
}
|
||||
|
||||
abstract static class GenericAbstractSuperclass<C> implements GenericInterface<Long, C> {
|
||||
|
||||
@Override
|
||||
public void processOne(Long value1) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processOneAndTwo(Long value1, C value2) {
|
||||
}
|
||||
|
||||
public abstract void processTwo(@Max(42) C value);
|
||||
}
|
||||
|
||||
static class GenericInterfaceImpl extends GenericAbstractSuperclass<String> {
|
||||
|
||||
@Override
|
||||
public void processTwo(String value) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user