mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-20 12:19:16 +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.AnnotationUtils;
|
||||||
import org.springframework.core.annotation.MergedAnnotation;
|
import org.springframework.core.annotation.MergedAnnotation;
|
||||||
import org.springframework.core.annotation.MergedAnnotationCollectors;
|
import org.springframework.core.annotation.MergedAnnotationCollectors;
|
||||||
import org.springframework.core.annotation.MergedAnnotationPredicates;
|
|
||||||
import org.springframework.core.annotation.MergedAnnotations;
|
import org.springframework.core.annotation.MergedAnnotations;
|
||||||
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||||
import org.springframework.core.annotation.RepeatableContainers;
|
import org.springframework.core.annotation.RepeatableContainers;
|
||||||
@@ -571,14 +570,21 @@ public abstract class TestContextAnnotationUtils {
|
|||||||
* that are present or meta-present on the {@linkplain #getRootDeclaringClass()
|
* 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} of this descriptor or on any interfaces that the
|
||||||
* root declaring class implements.
|
* 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,
|
* @return the set of all merged, synthesized {@code Annotations} found,
|
||||||
* or an empty set if none were found
|
* or an empty set if none were found
|
||||||
*/
|
*/
|
||||||
public Set<T> findAllLocalMergedAnnotations() {
|
public Set<T> findAllLocalMergedAnnotations() {
|
||||||
SearchStrategy searchStrategy = SearchStrategy.TYPE_HIERARCHY;
|
Class<?> rootDeclaringClass = getRootDeclaringClass();
|
||||||
return MergedAnnotations.from(getRootDeclaringClass(), searchStrategy, RepeatableContainers.none())
|
Set<Class<?>> localSources = new HashSet<>();
|
||||||
|
localSources.add(rootDeclaringClass);
|
||||||
|
Collections.addAll(localSources, rootDeclaringClass.getInterfaces());
|
||||||
|
return MergedAnnotations.from(rootDeclaringClass, SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.none())
|
||||||
.stream(getAnnotationType())
|
.stream(getAnnotationType())
|
||||||
.filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex))
|
.filter(mergedAnnotation -> localSources.contains(mergedAnnotation.getSource()))
|
||||||
.collect(MergedAnnotationCollectors.toAnnotationSet());
|
.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
|
@Nested
|
||||||
@DisplayName("findAnnotationDescriptorForTypes() tests")
|
@DisplayName("findAnnotationDescriptorForTypes() tests")
|
||||||
class FindAnnotationDescriptorForTypesTests {
|
class FindAnnotationDescriptorForTypesTests {
|
||||||
@@ -377,7 +444,7 @@ class TestContextAnnotationUtilsTests {
|
|||||||
assertThat(descriptor.getAnnotationType()).isEqualTo(annotationType);
|
assertThat(descriptor.getAnnotationType()).isEqualTo(annotationType);
|
||||||
assertThat(((ContextConfiguration) descriptor.getAnnotation()).value()).isEmpty();
|
assertThat(((ContextConfiguration) descriptor.getAnnotation()).value()).isEmpty();
|
||||||
assertThat(((ContextConfiguration) descriptor.getAnnotation()).classes())
|
assertThat(((ContextConfiguration) descriptor.getAnnotation()).classes())
|
||||||
.containsExactly(MetaConfig.DevConfig.class, MetaConfig.ProductionConfig.class);
|
.containsExactly(DevConfig.class, ProductionConfig.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -559,12 +626,12 @@ class TestContextAnnotationUtilsTests {
|
|||||||
@AliasFor(annotation = ContextConfiguration.class)
|
@AliasFor(annotation = ContextConfiguration.class)
|
||||||
Class<?>[] classes() default { DevConfig.class, ProductionConfig.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