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")