• We use a meta-annotation @As400Sql for the annotation @Sql. It is repeatable.
  • We annotate DoubleAnnotatedClass.class with two such annotations.
  • AnnotatedElementUtils.getMergedRepeatableAnnotations() does then not retreive the @Sql annotations.
  • When using an intermediate annotation (@TwoSql) it works as expected.

Observed in spring-core 5.2.8.RELEASE via spring-boot 2.3.3.RELEASE

public class As400SqlTest {

  @Target({ElementType.TYPE, ElementType.METHOD})
  @Retention(RetentionPolicy.RUNTIME)
  @Repeatable(As400SqlGroup.class)
  @Sql(config = @SqlConfig(dataSource = "as400DataSource", transactionManager =
      "as400TransactionManager"))
  public @interface As400Sql {

    @AliasFor(attribute = "scripts", annotation = Sql.class)
    String[] value() default {};
  }

  @Target({ElementType.TYPE, ElementType.METHOD})
  @Retention(RetentionPolicy.RUNTIME)
  public @interface As400SqlGroup {

    As400Sql[] value();
  }



  @As400Sql("script1")
  public static class AnnotatedClass {

  }

  @Test
  void getMergedRepeatableAnnotations_shouldReturnSql_forClassWithAs400SqlAnnotation() {
    Set<Sql> actual = AnnotatedElementUtils
        .getMergedRepeatableAnnotations(AnnotatedClass.class, Sql.class, SqlGroup.class);

    assertThat(actual).hasSize(1);
    Sql actual0 = actual.iterator().next();
    assertThat(actual0.scripts()).containsExactly("script1");
    assertThat(actual0.config().dataSource()).isEqualTo("as400DataSource");
  }


  @As400Sql("script1")
  @As400Sql("script2")
  public static class DoubleAnnotatedClass {

  }

  @Disabled("getMergedRepeatableAnnotations returns 0 annotations - why?")
  @Test
  void getMergedRepeatableAnnotations_shouldReturnTwoSql_forDoubleAnnotatedClass() {
    Set<Sql> actual = AnnotatedElementUtils
        .getMergedRepeatableAnnotations(DoubleAnnotatedClass.class, Sql.class, SqlGroup.class);

    assertThat(actual).hasSize(2);
    Iterator<Sql> actualElements = actual.iterator();
    assertThat(actualElements.next().config().dataSource()).isEqualTo("as400DataSource");
    assertThat(actualElements.next().config().dataSource()).isEqualTo("as400DataSource");
  }


  @As400Sql("script1")
  @As400Sql("script2")
  @Retention(RetentionPolicy.RUNTIME)
  public @interface TwoSql {

  }

  @TwoSql
  public static class MetaDoubleAnnotatedClass {

  }

  @Test
  void getMergedRepeatableAnnotations_shouldReturnTwoSql_forMetaDoubleAnnotatedClass() {
    Set<Sql> actual = AnnotatedElementUtils
        .getMergedRepeatableAnnotations(MetaDoubleAnnotatedClass.class, Sql.class, SqlGroup.class);

    assertThat(actual).hasSize(2);
    Iterator<Sql> actualElements = actual.iterator();
    assertThat(actualElements.next().config().dataSource()).isEqualTo("as400DataSource");
    assertThat(actualElements.next().config().dataSource()).isEqualTo("as400DataSource");
  }
}

Comment From: schaa0

Additional information about this issue:

Method overloads of AnnotatedElementUtils.getMergedRepeatableAnnotations() use two different types of RepeatableContainers: - ExplicitRepeatableContainers - StandardRepeatableContainers

ExplicitRepeatableContainers is used to extract repeatable annotations from the annotated element based on the provided container type. Since the provided container type is SqlGroup but multiple As400Sql annotations are composed into As400SqlGroup, the types don't match and the As400Sql annotations on DoubleAnnotatedClass are skipped. This happens in the failing test case getMergedRepeatableAnnotations_shouldReturnTwoSql_forDoubleAnnotatedClass.

Once the annotations are determined from the annotated element, further processing of the annotation hierarchy will use StandardRepeatableContainers which extracts repeatable annotations from any container type which are found as meta annotation. That's why test case getMergedRepeatableAnnotations_shouldReturnTwoSql_forMetaDoubleAnnotatedClass passes.

Using StandardRepeatableContainers for both steps gives the expected result, but requires modification in AnnotatedElementUtils:

private static MergedAnnotations getRepeatableAnnotations(AnnotatedElement element, @Nullable Class<? extends Annotation> containerType, Class<? extends Annotation> annotationType) {
    // Instead of: RepeatableContainers.of(annotationType, containerType);
    RepeatableContainers repeatableContainers = RepeatableContainers.standardRepeatables();
    return MergedAnnotations.from(element, SearchStrategy.INHERITED_ANNOTATIONS, repeatableContainers);
}

As an alternative, it's possible to define a separate utility method since the required API components are public. The following is an example using the get semantics.

class AnnotatedElementUtilsExt {

    static <A extends Annotation> Set<A> getAllMergedRepeatableAnnotations(AnnotatedElement element, Class<A> annotationType) {
        assertIsRepeatable(annotationType);
        return MergedAnnotations.from(element, MergedAnnotations.SearchStrategy.INHERITED_ANNOTATIONS, RepeatableContainers.standardRepeatables())
                .stream(annotationType)
                .sorted(highAggregateIndexesFirst())
                .collect(MergedAnnotationCollectors.toAnnotationSet());
    }

    private static <A extends Annotation> void assertIsRepeatable(Class<A> annotationType) {
        boolean isRepeatable = annotationType.isAnnotationPresent(Repeatable.class);
        Assert.isTrue(isRepeatable, () -> "Annotation type must be a repeatable annotation: " +
                "failed to resolve container type for " + annotationType.getName());
    }

    private static <A extends Annotation> Comparator<MergedAnnotation<A>> highAggregateIndexesFirst() {
        return Comparator.<MergedAnnotation<A>> comparingInt(MergedAnnotation::getAggregateIndex).reversed();
    }
}

Comment From: sbrannen

Hi @mischkes,

Sorry for taking such a long time to get to this issue.

The good news is that this has been resolved in the interim in conjunction with #20279.

Starting with Spring Framework 5.3.24, to get getMergedRepeatableAnnotations_shouldReturnTwoSql_forDoubleAnnotatedClass() to pass, you need to invoke getMergedRepeatableAnnotations() without specifying the containerType as follows.

Set<Sql> actual = 
        AnnotatedElementUtils.getMergedRepeatableAnnotations(DoubleAnnotatedClass.class, Sql.class);

I am therefore closing this as a:

  • duplicate of #20279