Use mapped annotation values when resolving mirrors
Update `TypeMappedAnnotation` mirror resolution logic so that mapped annotation values are also considered. Prior to this commit, mirrors in more complex meta-annotation scenarios would not resolve correctly. Specifically, given the following: @interface TestAliased { @AliasFor(attribute = "qualifier") String value() default ""; @AliasFor(attribute = "value") String qualifier() default ""; } @TestAliased @interface TestMetaAliased { @AliasFor(annotation = Aliased.class, attribute = "value") String value() default ""; } @TestMetaAliased("test") @interface TestComposed { } A merged `@TestAliased` annotation obtained from a `@TestComposed` root annotation would return a `value` and `qualifier` of "". This was because the "value" and "qualifier" mirrors needed to be resolved against the `@TestMetaAliased` meta-annotation. They cannot be resolved against the declared `@TestComposed` annotation because it does not have any attributes. Our previous tests only covered a single depth scenario where `@TestMetaAliased` was used directly on the annotated element. Closes gh-23767 Co-authored-by: Sam Brannen <sbrannen@pivotal.io>
This commit is contained in:
parent
b1a938fa24
commit
ad543dbee3
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -436,11 +436,15 @@ final class TypeMappedAnnotation<A extends Annotation> 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
|
||||
|
|
|
@ -673,6 +673,62 @@ class AnnotatedElementUtilsTests {
|
|||
assertComponentScanAttributes(ComponentScanWithBasePackagesAndValueAliasClass.class, "com.example.app.test");
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 5.2.1
|
||||
* @see <a href="https://github.com/spring-projects/spring-framework/issues/23767">#23767</a>
|
||||
*/
|
||||
@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 <a href="https://github.com/spring-projects/spring-framework/issues/23767">#23767</a>
|
||||
*/
|
||||
@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 <a href="https://github.com/spring-projects/spring-framework/issues/23767">#23767</a>
|
||||
*/
|
||||
@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 <a href="https://github.com/spring-projects/spring-framework/issues/23767">#23767</a>
|
||||
*/
|
||||
@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 {
|
||||
}
|
||||
|
|
|
@ -587,6 +587,24 @@ class MergedAnnotationsTests {
|
|||
assertThat(synthesizedAnnotation.qualifier()).isEqualTo(qualifier);
|
||||
}
|
||||
|
||||
@Test // gh-23767
|
||||
void getWithTypeHierarchyFromClassWithComposedMetaTransactionalAnnotation() {
|
||||
MergedAnnotation<AliasedTransactional> 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<AliasedTransactional> 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")
|
||||
|
|
Loading…
Reference in New Issue