mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 16:30:28 +00:00
Find transitive interface annotations in findAllLocalMergedAnnotations()
This commit picks up where ce718cf699 left off by ensuring that
findAllLocalMergedAnnotations() also finds annotations declared on
transitive interfaces — that is, on interfaces of the directly
implemented interfaces of the root declaring class.
The previous implementation filtered annotations from a single
TYPE_HIERARCHY search by checking whether the annotation's source was
either the root declaring class or one of its directly declared
interfaces. However, this excluded annotations inherited through an
interface chain such as First -> Second -> Third, where the annotation
is declared on Third but not on Second.
This commit replaces that approach with two targeted searches whose
results are combined:
- DIRECT on the root declaring class, to capture annotations declared
directly on the class (including via composed/meta-annotations)
- TYPE_HIERARCHY on each directly implemented interface, which
naturally traverses the full super-interface chain of each interface
A corresponding test for this scenario has also been added.
Closes gh-36975
This commit is contained in:
+10
-7
@@ -17,10 +17,12 @@
|
||||
package org.springframework.test.context;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
@@ -578,13 +580,14 @@ public abstract class TestContextAnnotationUtils {
|
||||
* or an empty set if none were found
|
||||
*/
|
||||
public Set<T> findAllLocalMergedAnnotations() {
|
||||
Class<?> rootDeclaringClass = getRootDeclaringClass();
|
||||
Set<Class<?>> localSources = new HashSet<>();
|
||||
localSources.add(rootDeclaringClass);
|
||||
Collections.addAll(localSources, rootDeclaringClass.getInterfaces());
|
||||
return MergedAnnotations.from(rootDeclaringClass, SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.none())
|
||||
.stream(getAnnotationType())
|
||||
.filter(mergedAnnotation -> localSources.contains(mergedAnnotation.getSource()))
|
||||
Class<T> annotationType = getAnnotationType();
|
||||
Stream<MergedAnnotation<T>> classAnnotations =
|
||||
MergedAnnotations.from(this.rootDeclaringClass, SearchStrategy.DIRECT, RepeatableContainers.none())
|
||||
.stream(annotationType);
|
||||
Stream<MergedAnnotation<T>> interfaceAnnotations = Arrays.stream(this.rootDeclaringClass.getInterfaces())
|
||||
.flatMap(ifc -> MergedAnnotations.from(ifc, SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.none())
|
||||
.stream(annotationType));
|
||||
return Stream.concat(classAnnotations, interfaceAnnotations)
|
||||
.collect(MergedAnnotationCollectors.toAnnotationSet());
|
||||
}
|
||||
|
||||
|
||||
+18
@@ -296,6 +296,17 @@ class TestContextAnnotationUtilsTests {
|
||||
.satisfies(config -> assertThat(config.classes()).containsExactly(Config2.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void annotationOnTransitiveInterface() {
|
||||
var descriptor = findAnnotationDescriptor(ClassImplementingTransitiveAnnotatedInterface.class, ContextConfiguration.class);
|
||||
assertThat(descriptor).isNotNull();
|
||||
assertThat(descriptor.findAllLocalMergedAnnotations())
|
||||
.satisfiesExactly(
|
||||
config1 -> assertThat(config1.classes()).containsExactly(Config1.class),
|
||||
config2 -> assertThat(config2.classes()).containsExactly(Config2.class)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void annotationOnClassAndInterface() {
|
||||
var descriptor = findAnnotationDescriptor(AnnotatedClassImplementingAnnotatedInterface.class, ContextConfiguration.class);
|
||||
@@ -765,6 +776,13 @@ class TestContextAnnotationUtilsTests {
|
||||
static class ClassImplementingAnnotatedInterface implements AnnotatedInterface {
|
||||
}
|
||||
|
||||
interface TransitiveAnnotatedInterface extends AnnotatedInterface {
|
||||
}
|
||||
|
||||
@ContextConfiguration(classes = Config1.class)
|
||||
static class ClassImplementingTransitiveAnnotatedInterface implements TransitiveAnnotatedInterface {
|
||||
}
|
||||
|
||||
@ContextConfiguration(classes = Config1.class)
|
||||
static class AnnotatedClassImplementingAnnotatedInterface implements AnnotatedInterface {
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user