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
|
* convention and alias based mapping rules. For root mappings, this method
|
||||||
* will always return {@code null}.
|
* will always return {@code null}.
|
||||||
* @param attributeIndex the attribute index of the source attribute
|
* @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}
|
* @return the mapped annotation value, or {@code null}
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
Object getMappedAnnotationValue(int attributeIndex) {
|
Object getMappedAnnotationValue(int attributeIndex, boolean metaAnnotationsOnly) {
|
||||||
int mapped = this.annotationValueMappings[attributeIndex];
|
int mapped = this.annotationValueMappings[attributeIndex];
|
||||||
if (mapped == -1) {
|
if (mapped == -1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
AnnotationTypeMapping source = this.annotationValueSource[attributeIndex];
|
AnnotationTypeMapping source = this.annotationValueSource[attributeIndex];
|
||||||
|
if(source == this && metaAnnotationsOnly) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return ReflectionUtils.invokeMethod(source.attributes.get(mapped), source.annotation);
|
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,
|
private Object getValueFromMetaAnnotation(int attributeIndex,
|
||||||
boolean forMirrorResolution) {
|
boolean forMirrorResolution) {
|
||||||
|
|
||||||
if (this.useMergedValues && !forMirrorResolution) {
|
Object value = null;
|
||||||
return this.mapping.getMappedAnnotationValue(attributeIndex);
|
if (this.useMergedValues || forMirrorResolution) {
|
||||||
|
value = this.mapping.getMappedAnnotationValue(attributeIndex, forMirrorResolution);
|
||||||
}
|
}
|
||||||
Method attribute = this.mapping.getAttributes().get(attributeIndex);
|
if (value == null) {
|
||||||
return ReflectionUtils.invokeMethod(attribute, this.mapping.getAnnotation());
|
Method attribute = mapping.getAttributes().get(attributeIndex);
|
||||||
|
value = ReflectionUtils.invokeMethod(attribute, mapping.getAnnotation());
|
||||||
|
}
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|
|
@ -673,6 +673,62 @@ class AnnotatedElementUtilsTests {
|
||||||
assertComponentScanAttributes(ComponentScanWithBasePackagesAndValueAliasClass.class, "com.example.app.test");
|
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
|
@Test
|
||||||
void findMergedAnnotationAttributesWithSingleElementOverridingAnArrayViaConvention() {
|
void findMergedAnnotationAttributesWithSingleElementOverridingAnArrayViaConvention() {
|
||||||
assertComponentScanAttributes(ConventionBasedSinglePackageComponentScanClass.class, "com.example.app.test");
|
assertComponentScanAttributes(ConventionBasedSinglePackageComponentScanClass.class, "com.example.app.test");
|
||||||
|
@ -882,6 +938,21 @@ class AnnotatedElementUtilsTests {
|
||||||
String qualifier() default "";
|
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")
|
@Transactional(qualifier = "composed1")
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target(ElementType.TYPE)
|
@Target(ElementType.TYPE)
|
||||||
|
@ -1185,6 +1256,14 @@ class AnnotatedElementUtilsTests {
|
||||||
static class AliasedTransactionalComponentClass {
|
static class AliasedTransactionalComponentClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ComposedMyAliasedTransactional
|
||||||
|
void composedTransactionalMethod() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@ComposedMyAliasedTransactional
|
||||||
|
static class ComposedTransactionalClass {
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
static class ClassWithInheritedAnnotation {
|
static class ClassWithInheritedAnnotation {
|
||||||
}
|
}
|
||||||
|
|
|
@ -587,6 +587,24 @@ class MergedAnnotationsTests {
|
||||||
assertThat(synthesizedAnnotation.qualifier()).isEqualTo(qualifier);
|
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
|
@Test
|
||||||
void getWithTypeHierarchyFromClassWithAttributeAliasInComposedAnnotationAndNestedAnnotationsInTargetAnnotation() {
|
void getWithTypeHierarchyFromClassWithAttributeAliasInComposedAnnotationAndNestedAnnotationsInTargetAnnotation() {
|
||||||
MergedAnnotation<?> annotation = testGetWithTypeHierarchy(
|
MergedAnnotation<?> annotation = testGetWithTypeHierarchy(
|
||||||
|
@ -2200,6 +2218,38 @@ class MergedAnnotationsTests {
|
||||||
@interface AliasedTransactionalComponent {
|
@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
|
@TxComposedWithOverride
|
||||||
// Override default "txMgr" from @TxComposedWithOverride with "localTxMgr"
|
// Override default "txMgr" from @TxComposedWithOverride with "localTxMgr"
|
||||||
@Transactional(qualifier = "localTxMgr")
|
@Transactional(qualifier = "localTxMgr")
|
||||||
|
|
Loading…
Reference in New Issue