diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java
index eebb9f6e3d..f1c2e126d3 100644
--- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java
+++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java
@@ -444,15 +444,21 @@ final class AnnotationTypeMapping {
* convention and alias based mapping rules. For root mappings, this method
* will always return {@code null}.
* @param attributeIndex the attribute index of the source attribute
+ * @param metaAnnotationsOnly if only meta annotations should be considered.
+ * If this parameter is {@code false} then aliases within the annotation will
+ * not be excluded.
* @return the mapped annotation value, or {@code null}
*/
@Nullable
- Object getMappedAnnotationValue(int attributeIndex) {
+ Object getMappedAnnotationValue(int attributeIndex, boolean metaAnnotationsOnly) {
int mapped = this.annotationValueMappings[attributeIndex];
if (mapped == -1) {
return null;
}
AnnotationTypeMapping source = this.annotationValueSource[attributeIndex];
+ if(source == this && metaAnnotationsOnly) {
+ return null;
+ }
return ReflectionUtils.invokeMethod(source.attributes.get(mapped), source.annotation);
}
diff --git a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java
index 0557743814..a47105eaba 100644
--- a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java
+++ b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java
@@ -436,11 +436,15 @@ final class TypeMappedAnnotation extends AbstractMergedAnn
private Object getValueFromMetaAnnotation(int attributeIndex,
boolean forMirrorResolution) {
- if (this.useMergedValues && !forMirrorResolution) {
- return this.mapping.getMappedAnnotationValue(attributeIndex);
+ Object value = null;
+ if (this.useMergedValues || forMirrorResolution) {
+ value = this.mapping.getMappedAnnotationValue(attributeIndex, forMirrorResolution);
}
- Method attribute = this.mapping.getAttributes().get(attributeIndex);
- return ReflectionUtils.invokeMethod(attribute, this.mapping.getAnnotation());
+ if (value == null) {
+ Method attribute = mapping.getAttributes().get(attributeIndex);
+ value = ReflectionUtils.invokeMethod(attribute, mapping.getAnnotation());
+ }
+ return value;
}
@Nullable
diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java
index 19a66da16a..4c0c108bbe 100644
--- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java
+++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java
@@ -673,6 +673,62 @@ class AnnotatedElementUtilsTests {
assertComponentScanAttributes(ComponentScanWithBasePackagesAndValueAliasClass.class, "com.example.app.test");
}
+ /**
+ * @since 5.2.1
+ * @see #23767
+ */
+ @Test
+ void findMergedAnnotationAttributesOnMethodWithComposedMetaTransactionalAnnotation() throws Exception {
+ Method method = getClass().getDeclaredMethod("composedTransactionalMethod");
+
+ AnnotationAttributes attributes = findMergedAnnotationAttributes(method, AliasedTransactional.class);
+ assertThat(attributes).as("Should find @AliasedTransactional on " + method).isNotNull();
+ assertThat(attributes.getString("value")).as("TX qualifier for " + method).isEqualTo("anotherTransactionManager");
+ assertThat(attributes.getString("qualifier")).as("TX qualifier for " + method).isEqualTo("anotherTransactionManager");
+ }
+
+ /**
+ * @since 5.2.1
+ * @see #23767
+ */
+ @Test
+ void findMergedAnnotationOnMethodWithComposedMetaTransactionalAnnotation() throws Exception {
+ Method method = getClass().getDeclaredMethod("composedTransactionalMethod");
+
+ AliasedTransactional annotation = findMergedAnnotation(method, AliasedTransactional.class);
+ assertThat(annotation).as("Should find @AliasedTransactional on " + method).isNotNull();
+ assertThat(annotation.value()).as("TX qualifier for " + method).isEqualTo("anotherTransactionManager");
+ assertThat(annotation.qualifier()).as("TX qualifier for " + method).isEqualTo("anotherTransactionManager");
+ }
+
+ /**
+ * @since 5.2.1
+ * @see #23767
+ */
+ @Test
+ void findMergedAnnotationAttributesOnClassWithComposedMetaTransactionalAnnotation() throws Exception {
+ Class> clazz = ComposedTransactionalClass.class;
+
+ AnnotationAttributes attributes = findMergedAnnotationAttributes(clazz, AliasedTransactional.class);
+ assertThat(attributes).as("Should find @AliasedTransactional on " + clazz).isNotNull();
+ assertThat(attributes.getString("value")).as("TX qualifier for " + clazz).isEqualTo("anotherTransactionManager");
+ assertThat(attributes.getString("qualifier")).as("TX qualifier for " + clazz).isEqualTo("anotherTransactionManager");
+ }
+
+ /**
+ * @since 5.2.1
+ * @see #23767
+ */
+ @Test
+ void findMergedAnnotationOnClassWithComposedMetaTransactionalAnnotation() throws Exception {
+ Class> clazz = ComposedTransactionalClass.class;
+
+ AliasedTransactional annotation = findMergedAnnotation(clazz, AliasedTransactional.class);
+ assertThat(annotation).as("Should find @AliasedTransactional on " + clazz).isNotNull();
+ assertThat(annotation.value()).as("TX qualifier for " + clazz).isEqualTo("anotherTransactionManager");
+ assertThat(annotation.qualifier()).as("TX qualifier for " + clazz).isEqualTo("anotherTransactionManager");
+ }
+
@Test
void findMergedAnnotationAttributesWithSingleElementOverridingAnArrayViaConvention() {
assertComponentScanAttributes(ConventionBasedSinglePackageComponentScanClass.class, "com.example.app.test");
@@ -882,6 +938,21 @@ class AnnotatedElementUtilsTests {
String qualifier() default "";
}
+ @AliasedTransactional
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.TYPE, ElementType.METHOD})
+ @interface MyAliasedTransactional {
+
+ @AliasFor(annotation = AliasedTransactional.class, attribute = "value")
+ String value() default "defaultTransactionManager";
+ }
+
+ @MyAliasedTransactional("anotherTransactionManager")
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.TYPE, ElementType.METHOD})
+ @interface ComposedMyAliasedTransactional {
+ }
+
@Transactional(qualifier = "composed1")
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@@ -1185,6 +1256,14 @@ class AnnotatedElementUtilsTests {
static class AliasedTransactionalComponentClass {
}
+ @ComposedMyAliasedTransactional
+ void composedTransactionalMethod() {
+ }
+
+ @ComposedMyAliasedTransactional
+ static class ComposedTransactionalClass {
+ }
+
@Transactional
static class ClassWithInheritedAnnotation {
}
diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java
index 310471d04d..5b6e77ed8a 100644
--- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java
+++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java
@@ -587,6 +587,24 @@ class MergedAnnotationsTests {
assertThat(synthesizedAnnotation.qualifier()).isEqualTo(qualifier);
}
+ @Test // gh-23767
+ void getWithTypeHierarchyFromClassWithComposedMetaTransactionalAnnotation() {
+ MergedAnnotation mergedAnnotation = MergedAnnotations.from(
+ ComposedTransactionalClass.class, SearchStrategy.TYPE_HIERARCHY).get(
+ AliasedTransactional.class);
+ assertThat(mergedAnnotation.getString("value")).isEqualTo("anotherTransactionManager");
+ assertThat(mergedAnnotation.getString("qualifier")).isEqualTo("anotherTransactionManager");
+ }
+
+ @Test // gh-23767
+ void getWithTypeHierarchyFromClassWithMetaMetaAliasedTransactional() {
+ MergedAnnotation mergedAnnotation = MergedAnnotations.from(
+ MetaMetaAliasedTransactionalClass.class, SearchStrategy.TYPE_HIERARCHY).get(
+ AliasedTransactional.class);
+ assertThat(mergedAnnotation.getString("value")).isEqualTo("meta");
+ assertThat(mergedAnnotation.getString("qualifier")).isEqualTo("meta");
+ }
+
@Test
void getWithTypeHierarchyFromClassWithAttributeAliasInComposedAnnotationAndNestedAnnotationsInTargetAnnotation() {
MergedAnnotation> annotation = testGetWithTypeHierarchy(
@@ -2200,6 +2218,38 @@ class MergedAnnotationsTests {
@interface AliasedTransactionalComponent {
}
+ @AliasedTransactional
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface MyAliasedTransactional {
+
+ @AliasFor(annotation = AliasedTransactional.class, attribute = "value")
+ String value() default "defaultTransactionManager";
+ }
+
+ @MyAliasedTransactional("anotherTransactionManager")
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.TYPE, ElementType.METHOD})
+ @interface ComposedMyAliasedTransactional {
+ }
+
+ @ComposedMyAliasedTransactional
+ static class ComposedTransactionalClass {
+ }
+
+ @AliasedTransactional("meta")
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface MetaAliasedTransactional {
+ }
+
+ @MetaAliasedTransactional
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface MetaMetaAliasedTransactional {
+ }
+
+ @MetaMetaAliasedTransactional
+ static class MetaMetaAliasedTransactionalClass {
+ }
+
@TxComposedWithOverride
// Override default "txMgr" from @TxComposedWithOverride with "localTxMgr"
@Transactional(qualifier = "localTxMgr")