diff --git a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java index ddd078b25cf3..3e789889ccac 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -410,7 +410,10 @@ private MergedAnnotation process( Annotation[] repeatedAnnotations = repeatableContainers.findRepeatedAnnotations(annotation); if (repeatedAnnotations != null) { - return doWithAnnotations(type, aggregateIndex, source, repeatedAnnotations); + MergedAnnotation result = doWithAnnotations(type, aggregateIndex, source, repeatedAnnotations); + if (result != null) { + return result; + } } AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( annotation.annotationType(), repeatableContainers, annotationFilter); diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java index 40b1fdf5b8f8..f1a80ee56e73 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java @@ -24,6 +24,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.AnnotatedElement; +import java.util.Arrays; import java.util.Set; import java.util.stream.Stream; @@ -175,7 +176,7 @@ void typeHierarchyWhenOnClassReturnsAnnotations() { } @Test - void typeHierarchyWhenWhenOnSuperclassReturnsAnnotations() { + void typeHierarchyWhenOnSuperclassReturnsAnnotations() { Set annotations = getAnnotations(null, PeteRepeat.class, SearchStrategy.TYPE_HIERARCHY, SubRepeatableClass.class); assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", @@ -240,6 +241,44 @@ void typeHierarchyAnnotationsWithLocalComposedAnnotationWhoseRepeatableMetaAnnot assertThat(annotationTypes).containsExactly(WithRepeatedMetaAnnotations.class, Noninherited.class, Noninherited.class); } + @Test // gh-32731 + void searchFindsRepeatableContainerAnnotationAndRepeatedAnnotations() { + Class clazz = StandardRepeatablesWithContainerWithMultipleAttributesTestCase.class; + + // NO RepeatableContainers + MergedAnnotations mergedAnnotations = MergedAnnotations.from(clazz, TYPE_HIERARCHY, RepeatableContainers.none()); + ContainerWithMultipleAttributes container = mergedAnnotations + .get(ContainerWithMultipleAttributes.class) + .synthesize(MergedAnnotation::isPresent).orElse(null); + assertThat(container).as("container").isNotNull(); + assertThat(container.name()).isEqualTo("enigma"); + RepeatableWithContainerWithMultipleAttributes[] repeatedAnnotations = container.value(); + assertThat(Arrays.stream(repeatedAnnotations).map(RepeatableWithContainerWithMultipleAttributes::value)) + .containsExactly("A", "B"); + Set set = + mergedAnnotations.stream(RepeatableWithContainerWithMultipleAttributes.class) + .collect(MergedAnnotationCollectors.toAnnotationSet()); + // Only finds the locally declared repeated annotation. + assertThat(set.stream().map(RepeatableWithContainerWithMultipleAttributes::value)) + .containsExactly("C"); + + // Standard RepeatableContainers + mergedAnnotations = MergedAnnotations.from(clazz, TYPE_HIERARCHY, RepeatableContainers.standardRepeatables()); + container = mergedAnnotations + .get(ContainerWithMultipleAttributes.class) + .synthesize(MergedAnnotation::isPresent).orElse(null); + assertThat(container).as("container").isNotNull(); + assertThat(container.name()).isEqualTo("enigma"); + repeatedAnnotations = container.value(); + assertThat(Arrays.stream(repeatedAnnotations).map(RepeatableWithContainerWithMultipleAttributes::value)) + .containsExactly("A", "B"); + set = mergedAnnotations.stream(RepeatableWithContainerWithMultipleAttributes.class) + .collect(MergedAnnotationCollectors.toAnnotationSet()); + // Finds the locally declared repeated annotation plus the 2 in the container. + assertThat(set.stream().map(RepeatableWithContainerWithMultipleAttributes::value)) + .containsExactly("A", "B", "C"); + } + private Set getAnnotations(Class container, Class repeatable, SearchStrategy searchStrategy, AnnotatedElement element) { @@ -449,4 +488,27 @@ static class SubNoninheritedRepeatableClass extends NoninheritedRepeatableClass static class WithRepeatedMetaAnnotationsClass { } + @Retention(RetentionPolicy.RUNTIME) + @interface ContainerWithMultipleAttributes { + + RepeatableWithContainerWithMultipleAttributes[] value(); + + String name() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(ContainerWithMultipleAttributes.class) + @interface RepeatableWithContainerWithMultipleAttributes { + + String value() default ""; + } + + @ContainerWithMultipleAttributes(name = "enigma", value = { + @RepeatableWithContainerWithMultipleAttributes("A"), + @RepeatableWithContainerWithMultipleAttributes("B") + }) + @RepeatableWithContainerWithMultipleAttributes("C") + static class StandardRepeatablesWithContainerWithMultipleAttributesTestCase { + } + }