From 6ee3ef6af5e215b854b0901e6a43737e86318c31 Mon Sep 17 00:00:00 2001
From: Sam Brannen <104798+sbrannen@users.noreply.github.com>
Date: Thu, 13 Aug 2026 14:55:39 +0200
Subject: [PATCH] Avoid unnecessarily synthesizing meta-annotations with
attributes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
In commit 622fc3edf7, I introduced a check in
TypeMappedAnnotation#isSynthesizable() intended to force synthesis when
an attribute value needs to be resolved from a different level of a
multi-level annotation hierarchy whose root annotation does not
redeclare the target attribute itself.
That check tested if `resolvedMirrors.length > 0` for a
meta-annotation; however, resolvedMirrors is always sized according to
the number of attributes declared by the mapped annotation type,
regardless of whether any of those attributes actually participate in
mirroring or an @AliasFor override. As a result, the check effectively
synthesized any meta-annotation that declares at least one attribute,
which reintroduced the unnecessary-synthesis behavior that commit
d6768ccc18 had fixed, merely narrowed to meta-annotations with
attributes.
This commit replaces that overly broad check with a precise one in
AnnotationTypeMapping#computeSynthesizableFlag(), which now also
considers whether any attribute's value must be resolved from a
different annotation in the meta-annotation hierarchy (tracked via
annotationValueSource). This correctly identifies the original
multi-level hierarchy scenario without over-matching on ordinary
meta-annotations that have nothing to merge or override.
See gh-28704
See gh-28716
Closes gh-37135
---
.../annotation/AnnotationTypeMapping.java | 11 ++++++
.../core/annotation/MergedAnnotation.java | 3 ++
.../core/annotation/TypeMappedAnnotation.java | 8 +----
.../annotation/MergedAnnotationsTests.java | 34 +++++++++++++++++++
4 files changed, 49 insertions(+), 7 deletions(-)
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 563341654e1..b423301fbf4 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
@@ -275,6 +275,17 @@ final class AnnotationTypeMapping {
return true;
}
+ // Does an attribute value need to be resolved from a different annotation
+ // in the meta-annotation hierarchy? This can happen when an attribute is
+ // overridden via @AliasFor further up a multi-level annotation hierarchy
+ // whose root annotation does not redeclare the target attribute itself,
+ // in which case the override cannot be tracked via aliasMappings/aliasedBy.
+ for (int i = 0; i < this.annotationValueSource.length; i++) {
+ if (this.annotationValueSource[i] != null && this.annotationValueSource[i] != this) {
+ return true;
+ }
+ }
+
// Has nested annotations or arrays of annotations that are synthesizable?
if (getAttributes().hasNestedAnnotation()) {
AttributeMethods attributeMethods = getAttributes();
diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java
index bed8407fc9d..8ae3b01a5fc 100644
--- a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java
+++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java
@@ -513,6 +513,9 @@ public interface MergedAnnotation {
* it has not already been synthesized and one of the following is true.
*
* - The annotation declares attributes annotated with {@link AliasFor @AliasFor}.
+ * - The annotation is used as a meta-annotation, and one of its attributes
+ * is overridden via an {@link AliasFor @AliasFor} declaration in a composed
+ * annotation somewhere in the annotation hierarchy.
* - The annotation declares attributes that are annotations or arrays of
* annotations that are themselves synthesizable.
*
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 f28f7760582..67d181818fe 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
@@ -354,8 +354,7 @@ final class TypeMappedAnnotation extends AbstractMergedAnn
/**
* Determine if the supplied annotation has not already been synthesized
- * and whether the mapped annotation is a composed annotation
- * that needs to have its attributes merged or the mapped annotation is
+ * and whether the mapped annotation is
* {@linkplain AnnotationTypeMapping#isSynthesizable() synthesizable} in general.
* @param annotation the annotation to check
* @since 5.3.22
@@ -365,11 +364,6 @@ final class TypeMappedAnnotation extends AbstractMergedAnn
if (AnnotationUtils.isSynthesizedAnnotation(annotation)) {
return false;
}
- // Is this a mapped annotation for a composed annotation, and are there
- // annotation attributes (mirrors) that need to be merged?
- if (getDistance() > 0 && this.resolvedMirrors.length > 0) {
- return true;
- }
// Is the mapped annotation itself synthesizable?
return this.mapping.isSynthesizable();
}
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 3c3b534a0f9..4aa1adff507 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
@@ -1565,6 +1565,22 @@ class MergedAnnotationsTests {
assertNotSynthesized(enableGlobalAuthentication);
}
+ /**
+ * A meta-annotation that declares an attribute should not be synthesized
+ * merely because it is meta-present rather than directly present, as long
+ * as none of its attributes are mirrored or overridden anywhere in the
+ * annotation hierarchy.
+ */
+ @Test
+ void synthesizeShouldNotSynthesizeMetaAnnotationsWithNonOverriddenAttributes() {
+ MergedAnnotations mergedAnnotations = MergedAnnotations.from(ComponentWithPlainAttributeMetaAnnotation.class);
+
+ PlainAttributeMetaAnnotation plainAttributeMetaAnnotation =
+ mergedAnnotations.get(PlainAttributeMetaAnnotation.class).synthesize();
+ assertThat(plainAttributeMetaAnnotation.value()).isEqualTo("enigma");
+ assertNotSynthesized(plainAttributeMetaAnnotation);
+ }
+
/**
* If an attempt is made to synthesize an annotation from an annotation instance
* that has already been synthesized, the original synthesized annotation should
@@ -3278,6 +3294,24 @@ class MergedAnnotationsTests {
static class SecurityConfig {
}
+ /**
+ * Meta-annotation that declares an attribute which is never mirrored or
+ * overridden anywhere in the annotation hierarchy.
+ */
+ @Retention(RUNTIME)
+ @interface PlainAttributeMetaAnnotation {
+ String value() default "enigma";
+ }
+
+ @PlainAttributeMetaAnnotation
+ @Retention(RUNTIME)
+ @interface ComposedPlainAttributeAnnotation {
+ }
+
+ @ComposedPlainAttributeAnnotation
+ static class ComponentWithPlainAttributeMetaAnnotation {
+ }
+
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@interface RootAnnotation {