diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java index c5b84c9021a..c33e0fd9514 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java @@ -453,6 +453,8 @@ public interface MergedAnnotations extends Iterable *

The provided annotations must all be * {@link MergedAnnotation#isDirectlyPresent() directly present} and must have * an {@link MergedAnnotation#getAggregateIndex() aggregate index} of {@code 0}. + * In addition, the provided collection must retain the source declaration order + * of the annotations — for example, a {@link java.util.List}. *

The resulting {@code MergedAnnotations} instance will contain both the * provided annotations and any meta-annotations that can be read using * reflection. diff --git a/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileAnnotationDelegate.java b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileAnnotationDelegate.java index 810636236da..d5ce6e21941 100644 --- a/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileAnnotationDelegate.java +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileAnnotationDelegate.java @@ -26,7 +26,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -43,6 +42,7 @@ import org.springframework.util.ClassUtils; * * @author Brian Clozel * @author Juergen Hoeller + * @author Sam Brannen * @since 7.0 */ abstract class ClassFileAnnotationDelegate { @@ -50,11 +50,11 @@ abstract class ClassFileAnnotationDelegate { static MergedAnnotations createMergedAnnotations( String className, RuntimeVisibleAnnotationsAttribute annotationAttribute, @Nullable ClassLoader classLoader) { - Set> annotations = annotationAttribute.annotations() + List> annotations = annotationAttribute.annotations() .stream() .map(ann -> createMergedAnnotation(className, ann, classLoader)) .filter(Objects::nonNull) - .collect(Collectors.toSet()); + .collect(Collectors.toList()); return MergedAnnotations.of(annotations); } diff --git a/spring-core/src/test/java/org/springframework/core/type/AbstractAnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AbstractAnnotationMetadataTests.java index f5d163797e9..48681542e4a 100644 --- a/spring-core/src/test/java/org/springframework/core/type/AbstractAnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/AbstractAnnotationMetadataTests.java @@ -261,7 +261,9 @@ public abstract class AbstractAnnotationMetadataTests { assertThat(get(WithDirectAnnotations.class).getAnnotations().stream()) .filteredOn(MergedAnnotation::isDirectlyPresent) .extracting(a -> a.getType().getName()) - .containsExactlyInAnyOrder(DirectAnnotation1.class.getName(), DirectAnnotation2.class.getName()); + // We do not use containsExactlyInAnyOrder(), because annotations + // must be returned in source declaration order. + .containsExactly(DirectAnnotation1.class.getName(), DirectAnnotation2.class.getName()); } @Test @@ -290,8 +292,10 @@ public abstract class AbstractAnnotationMetadataTests { MultiValueMap attributes = get(WithMetaAnnotationAttributes.class).getAllAnnotationAttributes(AnnotationAttributes.class.getName()); assertThat(attributes).containsOnlyKeys("name", "size"); - assertThat(attributes.get("name")).containsExactlyInAnyOrder("m1", "m2"); - assertThat(attributes.get("size")).containsExactlyInAnyOrder(1, 2); + // We do not use containsExactlyInAnyOrder(), because annotations + // must be returned in source declaration order. + assertThat(attributes.get("name")).containsExactly("m1", "m2"); + assertThat(attributes.get("size")).containsExactly(1, 2); } @Test @@ -335,15 +339,19 @@ public abstract class AbstractAnnotationMetadataTests { @Test void getAnnotationTypesReturnsDirectAnnotations() { AnnotationMetadata metadata = get(WithDirectAnnotations.class); - assertThat(metadata.getAnnotationTypes()).containsExactlyInAnyOrder( - DirectAnnotation1.class.getName(), DirectAnnotation2.class.getName()); + // We do not use containsExactlyInAnyOrder(), because annotations + // must be returned in source declaration order. + assertThat(metadata.getAnnotationTypes()) + .containsExactly(DirectAnnotation1.class.getName(), DirectAnnotation2.class.getName()); } @Test void getMetaAnnotationTypesReturnsMetaAnnotations() { AnnotationMetadata metadata = get(WithMetaAnnotations.class); + // We do not use containsExactlyInAnyOrder(), because annotations + // must be returned in source declaration order. assertThat(metadata.getMetaAnnotationTypes(MetaAnnotationRoot.class.getName())) - .containsExactlyInAnyOrder(MetaAnnotation1.class.getName(), MetaAnnotation2.class.getName()); + .containsExactly(MetaAnnotation1.class.getName(), MetaAnnotation2.class.getName()); } @Test diff --git a/spring-core/src/test/java/org/springframework/core/type/AbstractMethodMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AbstractMethodMetadataTests.java index e21dee73006..799626afd1b 100644 --- a/spring-core/src/test/java/org/springframework/core/type/AbstractMethodMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/AbstractMethodMetadataTests.java @@ -224,7 +224,9 @@ public abstract class AbstractMethodMetadataTests { Stream> types = metadata.getAnnotations().stream() .filter(MergedAnnotation::isDirectlyPresent) .map(MergedAnnotation::getType); - assertThat(types).containsExactlyInAnyOrder(Tag.class, DirectAnnotation.class); + // We do not use containsExactlyInAnyOrder(), because annotations + // must be returned in source declaration order. + assertThat(types).containsExactly(Tag.class, DirectAnnotation.class); } @Test @@ -254,8 +256,10 @@ public abstract class AbstractMethodMetadataTests { MultiValueMap attributes = getTagged(WithMetaAnnotationAttributes.class) .getAllAnnotationAttributes(AnnotationAttributes.class.getName()); assertThat(attributes).containsOnlyKeys("name", "size"); - assertThat(attributes.get("name")).containsExactlyInAnyOrder("m1", "m2"); - assertThat(attributes.get("size")).containsExactlyInAnyOrder(1, 2); + // We do not use containsExactlyInAnyOrder(), because annotations + // must be returned in source declaration order. + assertThat(attributes.get("name")).containsExactly("m1", "m2"); + assertThat(attributes.get("size")).containsExactly(1, 2); } @Test // gh-24375 diff --git a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java index 878734aaeac..1185a34ef38 100644 --- a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java @@ -405,7 +405,9 @@ class AnnotationMetadataTests { assertThat(metadata.hasAnnotation(SpecialAttr.class.getName())).isTrue(); assertThat(metadata.hasAnnotation(NamedComposedAnnotation.class.getName())).isTrue(); - assertThat(metadata.getAnnotationTypes()).containsExactlyInAnyOrder( + // We do not use containsExactlyInAnyOrder(), because annotations + // must be returned in source declaration order. + assertThat(metadata.getAnnotationTypes()).containsExactly( Component.class.getName(), Scope.class.getName(), SpecialAttr.class.getName(), DirectAnnotation.class.getName(), MetaMetaAnnotation.class.getName(), EnumSubclasses.class.getName(), diff --git a/spring-core/src/test/java/org/springframework/core/type/InheritedAnnotationsAnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/InheritedAnnotationsAnnotationMetadataTests.java index 83c819216de..e3c20e314c9 100644 --- a/spring-core/src/test/java/org/springframework/core/type/InheritedAnnotationsAnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/InheritedAnnotationsAnnotationMetadataTests.java @@ -57,7 +57,9 @@ class InheritedAnnotationsAnnotationMetadataTests { @Test void getAnnotationTypes() { - assertThat(standardMetadata.getAnnotationTypes()).containsExactlyInAnyOrder( + // We do not use containsExactlyInAnyOrder(), because annotations + // must be returned in source declaration order. + assertThat(standardMetadata.getAnnotationTypes()).containsExactly( NamedAnnotation3.class.getName(), InheritedComposedAnnotation.class.getName()); @@ -81,11 +83,14 @@ class InheritedAnnotationsAnnotationMetadataTests { Set metaAnnotationTypes; metaAnnotationTypes = standardMetadata.getMetaAnnotationTypes(InheritedComposedAnnotation.class.getName()); - assertThat(metaAnnotationTypes).containsExactlyInAnyOrder( - MetaAnnotation.class.getName(), + // We do not use containsExactlyInAnyOrder(), because annotations + // must be returned in source declaration order, with local annotations + // before meta-annotations. + assertThat(metaAnnotationTypes).containsExactly( NamedAnnotation1.class.getName(), NamedAnnotation2.class.getName(), - NamedAnnotation3.class.getName()); + NamedAnnotation3.class.getName(), + MetaAnnotation.class.getName()); metaAnnotationTypes = asmMetadata.getMetaAnnotationTypes(InheritedComposedAnnotation.class.getName()); assertThat(metaAnnotationTypes).isEmpty(); @@ -138,7 +143,10 @@ class InheritedAnnotationsAnnotationMetadataTests { annotationAttributes = standardMetadata.getAllAnnotationAttributes(NamedAnnotation3.class.getName()); assertThat(annotationAttributes).containsKey("name"); - assertThat(annotationAttributes.get("name")).containsExactlyInAnyOrder("name 3", "local"); + // We do not use containsExactlyInAnyOrder(), because annotations + // must be returned in source declaration order, with local annotations + // before meta-annotations. + assertThat(annotationAttributes.get("name")).containsExactly("local", "name 3"); annotationAttributes = asmMetadata.getAllAnnotationAttributes(NamedAnnotation1.class.getName()); assertThat(annotationAttributes).isNull();