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.