- 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