mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 08:24:13 +00:00
Always find interface annotations in findAllLocalMergedAnnotations()
Prior to this commit, AnnotationDescriptor's findAllLocalMergedAnnotations() filtered results using MergedAnnotationPredicates.firstRunOf( MergedAnnotation::getAggregateIndex), which retains only annotations that share the same aggregate index as the first annotation encountered. Since annotations on the root declaring class have aggregate index 0 and annotations on interfaces have higher indices, interface annotations were silently excluded whenever the root declaring class itself declared the annotation. This commit fixes the issue by replacing the aggregate-index-based filter with a source-based filter that explicitly includes annotations from the root declaring class and from each directly implemented interface. The Javadoc for findAllLocalMergedAnnotations() has also been updated to document the ordering guarantee: annotations from the root declaring class appear first, followed by annotations from implemented interfaces in declaration order. Closes gh-36975
This commit is contained in:
+10
-4
@@ -29,7 +29,6 @@ import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.annotation.MergedAnnotationCollectors;
|
||||
import org.springframework.core.annotation.MergedAnnotationPredicates;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||
import org.springframework.core.annotation.RepeatableContainers;
|
||||
@@ -571,14 +570,21 @@ public abstract class TestContextAnnotationUtils {
|
||||
* that are present or meta-present on the {@linkplain #getRootDeclaringClass()
|
||||
* root declaring class} of this descriptor or on any interfaces that the
|
||||
* root declaring class implements.
|
||||
* <p>Annotations are returned in the order they are discovered: annotations
|
||||
* on the root declaring class appear first, followed by annotations on
|
||||
* implemented interfaces in the order they are declared in the
|
||||
* {@code implements} clause.
|
||||
* @return the set of all merged, synthesized {@code Annotations} found,
|
||||
* or an empty set if none were found
|
||||
*/
|
||||
public Set<T> findAllLocalMergedAnnotations() {
|
||||
SearchStrategy searchStrategy = SearchStrategy.TYPE_HIERARCHY;
|
||||
return MergedAnnotations.from(getRootDeclaringClass(), searchStrategy, RepeatableContainers.none())
|
||||
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(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex))
|
||||
.filter(mergedAnnotation -> localSources.contains(mergedAnnotation.getSource()))
|
||||
.collect(MergedAnnotationCollectors.toAnnotationSet());
|
||||
}
|
||||
|
||||
|
||||
+113
-5
@@ -274,6 +274,73 @@ class TestContextAnnotationUtilsTests {
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("findAllLocalMergedAnnotations() tests")
|
||||
class FindAllLocalMergedAnnotationsTests {
|
||||
|
||||
@Test
|
||||
void annotationOnClassOnly() {
|
||||
var descriptor = findAnnotationDescriptor(AnnotatedClass.class, ContextConfiguration.class);
|
||||
assertThat(descriptor).isNotNull();
|
||||
assertThat(descriptor.findAllLocalMergedAnnotations())
|
||||
.singleElement()
|
||||
.satisfies(config -> assertThat(config.classes()).containsExactly(Config1.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void annotationOnInterfaceOnly() {
|
||||
var descriptor = findAnnotationDescriptor(ClassImplementingAnnotatedInterface.class, ContextConfiguration.class);
|
||||
assertThat(descriptor).isNotNull();
|
||||
assertThat(descriptor.findAllLocalMergedAnnotations())
|
||||
.singleElement()
|
||||
.satisfies(config -> assertThat(config.classes()).containsExactly(Config2.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void annotationOnClassAndInterface() {
|
||||
var descriptor = findAnnotationDescriptor(AnnotatedClassImplementingAnnotatedInterface.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 annotationOnTwoInterfaces() {
|
||||
var descriptor = findAnnotationDescriptor(ClassImplementingTwoAnnotatedInterfaces.class, ContextConfiguration.class);
|
||||
assertThat(descriptor).isNotNull();
|
||||
assertThat(descriptor.findAllLocalMergedAnnotations())
|
||||
.satisfiesExactly(
|
||||
config1 -> assertThat(config1.classes()).containsExactly(Config2.class),
|
||||
config2 -> assertThat(config2.classes()).containsExactly(Config3.class)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void annotationOnSuperclassIsExcluded() {
|
||||
var descriptor = findAnnotationDescriptor(SubAnnotatedClass.class, ContextConfiguration.class);
|
||||
assertThat(descriptor).isNotNull();
|
||||
assertThat(descriptor.getRootDeclaringClass()).isEqualTo(AnnotatedClass.class);
|
||||
assertThat(descriptor.findAllLocalMergedAnnotations())
|
||||
.singleElement()
|
||||
.satisfies(config -> assertThat(config.classes()).containsExactly(Config1.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void metaAnnotationOnClassAndAnnotationOnInterface() {
|
||||
var descriptor = findAnnotationDescriptor(MetaAnnotatedClassImplementingAnnotatedInterface.class, ContextConfiguration.class);
|
||||
assertThat(descriptor).isNotNull();
|
||||
assertThat(descriptor.findAllLocalMergedAnnotations())
|
||||
.satisfiesExactly(
|
||||
config1 -> assertThat(config1.classes()).containsExactly(DevConfig.class, ProductionConfig.class),
|
||||
config2 -> assertThat(config2.classes()).containsExactly(Config2.class)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("findAnnotationDescriptorForTypes() tests")
|
||||
class FindAnnotationDescriptorForTypesTests {
|
||||
@@ -377,7 +444,7 @@ class TestContextAnnotationUtilsTests {
|
||||
assertThat(descriptor.getAnnotationType()).isEqualTo(annotationType);
|
||||
assertThat(((ContextConfiguration) descriptor.getAnnotation()).value()).isEmpty();
|
||||
assertThat(((ContextConfiguration) descriptor.getAnnotation()).classes())
|
||||
.containsExactly(MetaConfig.DevConfig.class, MetaConfig.ProductionConfig.class);
|
||||
.containsExactly(DevConfig.class, ProductionConfig.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -559,12 +626,12 @@ class TestContextAnnotationUtilsTests {
|
||||
@AliasFor(annotation = ContextConfiguration.class)
|
||||
Class<?>[] classes() default { DevConfig.class, ProductionConfig.class };
|
||||
|
||||
class DevConfig {
|
||||
}
|
||||
}
|
||||
|
||||
class ProductionConfig {
|
||||
}
|
||||
static class DevConfig {
|
||||
}
|
||||
|
||||
static class ProductionConfig {
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -668,4 +735,45 @@ class TestContextAnnotationUtilsTests {
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Fixtures for FindAllLocalMergedAnnotationsTests
|
||||
|
||||
static class Config1 {
|
||||
}
|
||||
|
||||
static class Config2 {
|
||||
}
|
||||
|
||||
static class Config3 {
|
||||
}
|
||||
|
||||
@ContextConfiguration(classes = Config2.class)
|
||||
interface AnnotatedInterface {
|
||||
}
|
||||
|
||||
@ContextConfiguration(classes = Config3.class)
|
||||
interface AnotherAnnotatedInterface {
|
||||
}
|
||||
|
||||
@ContextConfiguration(classes = Config1.class)
|
||||
static class AnnotatedClass {
|
||||
}
|
||||
|
||||
static class SubAnnotatedClass extends AnnotatedClass {
|
||||
}
|
||||
|
||||
static class ClassImplementingAnnotatedInterface implements AnnotatedInterface {
|
||||
}
|
||||
|
||||
@ContextConfiguration(classes = Config1.class)
|
||||
static class AnnotatedClassImplementingAnnotatedInterface implements AnnotatedInterface {
|
||||
}
|
||||
|
||||
static class ClassImplementingTwoAnnotatedInterfaces implements AnnotatedInterface, AnotherAnnotatedInterface {
|
||||
}
|
||||
|
||||
@MetaConfig
|
||||
static class MetaAnnotatedClassImplementingAnnotatedInterface implements AnnotatedInterface {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user