Liam Bryan opened SPR-15723 and commented
Meta annotations are not detected when present on Repeatable
annotations.
Provided zip contains a quick example highlighting this, but for a contained example here (@Retention
and @Target
annotations omitted):
@Repeatable(A.List.class)
@interface A {
int value() default 1;
@interface List {
A[] value();
}
}
@Repeatable(B.List.class)
@A
@interface B {
@AliasFor(annotation = A.class, attribute = "value")
int value();
@interface List {
B[] value();
}
}
None of the provided methods in AnnotationUtils
or AnnotatedElementUtils
can locate the meta-@A
annotations for an element with repeated @B
annotations.
Affects: 4.3.8
Attachments: - example.zip (10.87 kB)
Comment From: sbrannen
Thanks for raising the issue, and I apologize that we took so long to triage it.
I was able to reproduce this against master
with the following all-in-one test class.
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
@SuppressWarnings("unused")
class AnnotationRetrievalTest {
@Test
void findAnnotationsSingle() throws Exception {
Method singleAnnotatedMethod = getClass().getDeclaredMethod("singleAnnotatedMethod");
// Passes.
performTest(singleAnnotatedMethod, 1);
}
@Test
void findAnnotationsMulti() throws Exception {
Method multiAnnotatedMethod = getClass().getDeclaredMethod("multiAnnotatedMethod");
// Fails (for all 3 sub-assertions).
performTest(multiAnnotatedMethod, 2);
}
private void performTest(Method method, int expectedAnnotationCount) {
Set<A> fromFindMergedRepeatable = AnnotatedElementUtils.findMergedRepeatableAnnotations(method, A.class);
Set<A> fromFindMergedRepeatableWithContainer = AnnotatedElementUtils.findMergedRepeatableAnnotations(method,
A.class, A.Container.class);
Set<A> fromGetRepeatable = AnnotationUtils.getRepeatableAnnotations(method, A.class);
List<A> fromJUnitFindRepeatable = org.junit.platform.commons.util.AnnotationUtils
.findRepeatableAnnotations(method, A.class);
assertAll(() -> assertEquals(expectedAnnotationCount, fromFindMergedRepeatable.size()),
() -> assertEquals(expectedAnnotationCount, fromFindMergedRepeatableWithContainer.size()),
() -> assertEquals(expectedAnnotationCount, fromGetRepeatable.size()),
() -> assertEquals(expectedAnnotationCount, fromJUnitFindRepeatable.size()));
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Repeatable(A.Container.class)
public @interface A {
int value() default 0;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@interface Container {
A[] value();
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Repeatable(B.Container.class)
@A
public @interface B {
@AliasFor(annotation = A.class)
int value();
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@interface Container {
B[] value();
}
}
@B(5)
void singleAnnotatedMethod() {
}
@B(5)
@B(10)
void multiAnnotatedMethod() {
}
}
Interestingly, the supplied example.zip
was testing org.junit.platform.commons.util.AnnotationUtils.findRepeatableAnnotations
from JUnit 5 instead of org.springframework.core.annotation.AnnotationUtils.getRepeatableAnnotations
from Spring. In any case, the error is the same. Neither Spring nor JUnit 5 find the repeatable annotation for the "multi" scenario.
@philwebb and @jhoeller, do you think we should try to add support for this scenario?
Comment From: sbrannen
Actually, after having put further thought into it, the expectation for AnnotationUtils.getRepeatableAnnotations
(as well as for JUnit's similar method) is invalid.
getRepeatableAnnotations
does not merge annotation attributes. Thus, the algorithm may encounter @A
twice, but each encounter is seen as the same instance. The @A
annotation is therefore only found once. Consequently, the expectation should be 1
instead of 2
for the non-merging search algorithms.
Comment From: sbrannen
Reopening to address the same issue in AnnotatedElementUtils.getMergedRepeatableAnnotations()
.
Comment From: sbrannen
It turns out that we already had support for finding repeatable annotations used as meta-annotations on other repeatable annotations.
This is possible via the MergedAnnotations
API.
However, the way that RepeatableContainers
were previously configured in AnnotatedElementUtils
for the getMergedRepeatableAnnotations()
and findMergedRepeatableAnnotations()
methods effectively limited the annotation search in a way that it only supported one type of repeatable annotation.
I fixed this in 828f74f71a068f30b9c158f2a182f7fb9dc50b5e and 9876701493717704cfe6e8258c6f1d6ce0c016e1.
See the commit messages as well as the associated tests for details.