Retain source declaration order in AnnotatedTypeMetadata on Java 24+

Prior to this commit, ClassFileAnnotationDelegate created
MergedAnnotations from a HashSet, which resulted in a non-deterministic
iteration order and lost the original source declaration order of the
annotations.

To address that, this commit revises ClassFileAnnotationDelegate to
create MergedAnnotations from a List.

In addition, this commit updates all related tests to use the
containsExactly() assertion instead of containsExactlyInAnyOrder() to
ensure we consistently adhere to the "source declaration order"
requirement.

Closes gh-36598
This commit is contained in:
Sam Brannen
2026-04-04 17:19:25 +02:00
parent c04b502866
commit 38464a15dc
6 changed files with 42 additions and 18 deletions
@@ -453,6 +453,8 @@ public interface MergedAnnotations extends Iterable<MergedAnnotation<Annotation>
* <p>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 &mdash; for example, a {@link java.util.List}.
* <p>The resulting {@code MergedAnnotations} instance will contain both the
* provided annotations and any meta-annotations that can be read using
* reflection.
@@ -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<MergedAnnotation<?>> annotations = annotationAttribute.annotations()
List<MergedAnnotation<?>> annotations = annotationAttribute.annotations()
.stream()
.map(ann -> createMergedAnnotation(className, ann, classLoader))
.filter(Objects::nonNull)
.collect(Collectors.toSet());
.collect(Collectors.toList());
return MergedAnnotations.of(annotations);
}
@@ -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<String, Object> 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
@@ -224,7 +224,9 @@ public abstract class AbstractMethodMetadataTests {
Stream<Class<?>> 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<String, Object> 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
@@ -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(),
@@ -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<String> 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();